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 sequelize@6.5.1 sqlite3@5.0.2.
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);
Conclusion: force
force: false
: if the table exists, nothing happens to it due to IF NOT EXISTS
in CREATE TABLE IF NOT EXISTS
. Therefore any table schema modifications are ignored.
force: true
: the table is dropped first, so any schema changes take effect CREATE TABLE IF NOT EXISTS
. But you lose all data
alter: true, force: false
actually attempts to keep data and modify the database
With force: true
, the database gets dropped and recreated, so of course it matches the latest schema, but you lose all data.
To try to 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');
function getSequelize() {
if (process.argv[2] === 'p') {
return new Sequelize('tmp', undefined, undefined, {
dialect: 'postgres',
host: '/var/run/postgresql',
})
} else {
return new Sequelize({
dialect: 'sqlite',
storage: 'tmp.sqlite',
})
}
}
(async () => {
{
const sequelize = getSequelize()
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 = getSequelize()
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
On PostgreSQL it actually produces ALTER
statements, which is awesome:
Executing (default): ALTER TABLE "public"."IntegerNames" ADD COLUMN "nameEs" VARCHAR(255);
Executing (default): ALTER TABLE "IntegerNames" ALTER COLUMN "value" DROP NOT NULL;ALTER TABLE "IntegerNames" ALTER COLUMN "value" DROP DEFAULT;ALTER TABLE "IntegerNames" ALTER COLUMN "value" TYPE INTEGER;
Executing (default): ALTER TABLE "IntegerNames" ALTER COLUMN "name" DROP NOT NULL;ALTER TABLE "IntegerNames" ALTER COLUMN "name" DROP DEFAULT;ALTER TABLE "IntegerNames" ALTER COLUMN "name" TYPE VARCHAR(255);
Executing (default): ALTER TABLE "IntegerNames" ALTER COLUMN "createdAt" SET NOT NULL;ALTER TABLE "IntegerNames" ALTER COLUMN "createdAt" DROP DEFAULT;ALTER TABLE "IntegerNames" ALTER COLUMN "createdAt" TYPE TIMESTAMP WITH TIME ZONE;
Executing (default): ALTER TABLE "IntegerNames" ALTER COLUMN "updatedAt" SET NOT NULL;ALTER TABLE "IntegerNames" ALTER COLUMN "updatedAt" DROP DEFAULT;ALTER TABLE "IntegerNames" ALTER COLUMN "updatedAt" TYPE TIMESTAMP WITH TIME ZONE;
We see that it just runs an ALTER
statement for every column separately, even if the column already exists, in which case it does not get modified in any way.
On SQLite, 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));
TODO why doesn't it use ALTER
statements? SQLite seems to support them: How do you rename a table in SQLite 3.0? 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.
Conclusion: alter
alter: true
allows us to easily add new columns, or modify properties of existing columns.
Reasons why this cannot fully replace migrations: Sequelize: Changing model schema on production
- it simply cannot handle table or column renames, it just doesn't have enough information to know that the old and new column are the same. I think it would also not handle column deletions on PostgreSQL.
- it cannot intelligently add/remove/modify data as might be required during a migration (obviously). As a simple example, adding a
NOT NULL
constraint may require you to fill in some previously NULL buggy rows with some value of choice.