How does sequelize.sync() work, specifically the force option?

(More or less) formal docs and API reference can be found at http://sequelize.readthedocs.org/en/latest/api/sequelize/#sync

To your question: force: true adds a DROP TABLE IF EXISTS before trying to create the table - if you force, existing tables will be overwritten.


The OP was asking what force: false does, which is what I wanted to know too, so here's the rest.

The major takeaway, for me, was that the individual fields aren't synced (which is what I was hoping for, coming from the Waterline ORM). Meaning, if you have force: false and the table exists, any field additions/modifications/deletions you have won't be executed.

  • beforeSync hooks are run
  • table is dropped if force: true
  • table is created with if not exists
  • indexes are added if necessary
  • afterSync hooks are run

Here's the current code from the github repo for reference:

lib.model.js

Model.prototype.sync = function(options) {
  options = options || {};
  options.hooks = options.hooks === undefined ? true : !!options.hooks;
  options = Utils._.extend({}, this.options, options);

  var self = this
    , attributes = this.tableAttributes;

  return Promise.try(function () {
    if (options.hooks) {
      return self.runHooks('beforeSync', options);
    }
  }).then(function () {
    if (options.force) {
      return self.drop(options);
    }
  }).then(function () {
    return self.QueryInterface.createTable(self.getTableName(options), attributes, options, self);
  }).then(function () {
    return self.QueryInterface.showIndex(self.getTableName(options), options);
  }).then(function (indexes) {
    // Assign an auto-generated name to indexes which are not named by the user
    self.options.indexes = self.QueryInterface.nameIndexes(self.options.indexes, self.tableName);

    indexes = _.filter(self.options.indexes, function (item1) {
      return !_.some(indexes, function (item2) {
        return item1.name === item2.name;
      });
    });

    return Promise.map(indexes, function (index) {
      return self.QueryInterface.addIndex(self.getTableName(options), _.assign({logging: options.logging, benchmark: options.benchmark}, index), self.tableName);
    });
  }).then(function () {
    if (options.hooks) {
      return self.runHooks('afterSync', options);
    }
  }).return(this);
};

Minimal runnable example

const { Sequelize, DataTypes } = require('sequelize');
const sequelize = new Sequelize({
  dialect: 'sqlite',
  storage: 'tmp.sqlite',
});
(async () => {
const IntegerNames = sequelize.define('IntegerNames', {
  value: { type: DataTypes.INTEGER, },
  name: { type: DataTypes.STRING, },
}, {});
//await IntegerNames.sync({force: true})
await IntegerNames.create({value: 2, name: 'two'});
await sequelize.close();
})();

Setup:

npm install [email protected] [email protected].

On stdout we can see the queries it did:

Executing (default): DROP TABLE IF EXISTS `IntegerNames`;
Executing (default): CREATE TABLE IF NOT EXISTS `IntegerNames` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `value` INTEGER, `name` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
Executing (default): PRAGMA INDEX_LIST(`IntegerNames`)
Executing (default): INSERT INTO `IntegerNames` (`id`,`value`,`name`,`createdAt`,`updatedAt`) VALUES (NULL,$1,$2,$3,$4);

If we use force: false instead we get the same except there's no DROP at the start:

Executing (default): CREATE TABLE IF NOT EXISTS `IntegerNames` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `value` INTEGER, `name` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL);
Executing (default): PRAGMA INDEX_LIST(`IntegerNames`)
Executing (default): INSERT INTO `IntegerNames` (`id`,`value`,`name`,`createdAt`,`updatedAt`) VALUES (NULL,$1,$2,$3,$4);

And if we remove sync the table does not get created at all:

Executing (default): INSERT INTO `IntegerNames` (`id`,`value`,`name`,`createdAt`,`updatedAt`) VALUES (NULL,$1,$2,$3,$4);

Tested with:

npm install [email protected] [email protected].

force: false alone does not sync the schema, you need alter: true

With force: true, the database gets dropped and recreated, so of course it matches the latest schema, but you lose all data.

To both keep existing data and update the schema, we have to use alter: true in addition to force: false.

The following works and stdout shows the convoluted sequence of queries used to make it work by creating a temporary database IntegerNames_backup:

const assert = require('assert')
const { Sequelize, DataTypes } = require('sequelize');

(async () => {
{
  const sequelize = new Sequelize({
    dialect: 'sqlite',
    storage: 'tmp.sqlite',
  });
  const IntegerNames = sequelize.define('IntegerNames', {
    value: { type: DataTypes.INTEGER, },
    name: { type: DataTypes.STRING, },
  }, {});
  await IntegerNames.sync({force: true})
  await IntegerNames.create({value: 2, name: 'two'});
  await IntegerNames.create({value: 3, name: 'three'});
  await sequelize.close();
}

// Alter by adding column..
{
  const sequelize = new Sequelize({
    dialect: 'sqlite',
    storage: 'tmp.sqlite',
  });
  const IntegerNames = sequelize.define('IntegerNames', {
    value: { type: DataTypes.INTEGER, },
    name: { type: DataTypes.STRING, },
    nameEs: { type: DataTypes.STRING, },
  }, {});
  await IntegerNames.sync({
    alter: true,
    force: false,
  })
  await IntegerNames.create({value: 5, name: 'five' , nameEs: 'cinco'});
  await IntegerNames.create({value: 7, name: 'seven', nameEs: 'siete'});
  const integerNames = await IntegerNames.findAll({
    order: [['value', 'ASC']],
  });
  assert(integerNames[0].value  === 2);
  assert(integerNames[0].name   === 'two');
  assert(integerNames[0].nameEs === null);
  assert(integerNames[1].name   === 'three');
  assert(integerNames[1].nameEs === null);
  assert(integerNames[2].name   === 'five');
  assert(integerNames[2].nameEs === 'cinco');
  assert(integerNames[3].name   === 'seven');
  assert(integerNames[3].nameEs === 'siete');
  await sequelize.close();
}
})();

If we remove alter: true it blows up because the new column doe not exist:

SequelizeDatabaseError: SQLITE_ERROR: table IntegerNames has no column named nameEs

The way alter works is by creating a new table, and moving all data from the old table to the new one through:

Executing (default): CREATE TABLE IF NOT EXISTS `IntegerNames_backup` (`id` INTEGER PRIMARY KEY, `value` INTEGER, `name` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `nameEs` VARCHAR(255));
Executing (default): INSERT INTO `IntegerNames_backup` SELECT `id`, `value`, `name`, `createdAt`, `updatedAt`, `nameEs` FROM `IntegerNames`;
Executing (default): DROP TABLE `IntegerNames`;
Executing (default): CREATE TABLE IF NOT EXISTS `IntegerNames` (`id` INTEGER PRIMARY KEY, `value` INTEGER, `name` VARCHAR(255), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `nameEs` VARCHAR(255));

It does not user ALTER statements because SQLite does not currently support them. This then breaks constraints which sequelize does not automatically drop and recreate as needed: Process of changing a table with sequelize migration if foreign key constraint is active it is quite sad.

Migrations tend to be the recommended way however of updating the schema in real projects: Sequelize: Changing model schema on production