Knex vs Sequelize vs TypeORM vs MikroORM vs Drizzle Migrations
How five Node.js tools write, track and run database migrations, with a real migration file for each, a comparison table and advice on which one fits your project.
JavaScript migration files exporting up and down functions built on the Knex schema builder. SchemaDrawer runs Knex 3.3 with pg against a throw-away database of the engine you choose and returns an SVG of the schema it produces.
Upload the migration files directly at the top level. Each file exports up and down functions built on knex.schema.
The sample below is the PetClinic schema written for Knex.js, one of the sets SchemaDrawer's own tests draw on every engine:
migrations/ ├── 20240101000001_create_owners_and_pets.js ├── 20240101000002_create_vets.js └── 20240101000003_create_visits.js
.js, .cjs, .mjs, .ts, .cts and .mts; TypeScript runs through Node's type stripping, so keep to erasable syntax.
knex migrate:latest with a knexfile generated for the chosen engine. The database is a fresh PostgreSQL, MySQL, MariaDB, SQL Server, Oracle or SQLite container in the version you ask for; nothing of yours is touched.
PostgreSQL 10, 11, 12, 13, 14, 15, 16, 17, 18 MySQL 8.0, 8.4, 9 MariaDB 10.6, 11.4, 11.8, 12 SQL Server 2017, 2019, 2022, 2025 Oracle 21, 23 SQLite
migrationType=KNEXOwners, pets, types, vets, specialties and visits: the same seven tables every sample set on this site creates. These are the exact files the tests upload.
/**
* Owners, pet types and pets.
*
* Written with Knex's own schema builder, which is what a Knex migration
* normally is: the same file emits PostgreSQL and MySQL DDL, so one upload
* draws on both engines.
*
* Named constraints are passed as a plain string - table.primary(cols, name).
* The documented object form, table.primary(cols, { constraintName: name }),
* is rendered wrongly by MySQL's table compiler inside createTable (knex
* 3.3.0): it wraps the whole object and emits
* `constraint \`pk_owners\` as \`constraintName\` primary key (...)`.
*/
exports.up = async function up(knex) {
await knex.schema.createTable('owners', (table) => {
table.integer('id').notNullable();
table.string('first_name', 50).notNullable();
table.string('last_name', 50).notNullable();
table.string('address', 255);
table.string('city', 80);
table.string('telephone', 20);
table.primary(['id'], 'pk_owners');
});
await knex.schema.createTable('types', (table) => {
table.integer('id').notNullable();
table.string('name', 80).notNullable();
table.primary(['id'], 'pk_types');
});
await knex.schema.createTable('pets', (table) => {
table.integer('id').notNullable();
table.string('name', 50).notNullable();
table.date('birth_date');
table.integer('type_id').notNullable();
table.integer('owner_id').notNullable();
table.primary(['id'], 'pk_pets');
table.foreign('type_id', 'fk_pets_types').references('id').inTable('types');
table.foreign('owner_id', 'fk_pets_owners').references('id').inTable('owners');
});
};
exports.down = async function down(knex) {
await knex.schema.dropTableIfExists('pets');
await knex.schema.dropTableIfExists('types');
await knex.schema.dropTableIfExists('owners');
};
/** Vets, specialties and the join table between them. */
exports.up = async function up(knex) {
await knex.schema.createTable('vets', (table) => {
table.integer('id').notNullable();
table.string('first_name', 50).notNullable();
table.string('last_name', 50).notNullable();
table.primary(['id'], 'pk_vets');
});
await knex.schema.createTable('specialties', (table) => {
table.integer('id').notNullable();
table.string('name', 80).notNullable();
table.primary(['id'], 'pk_specialties');
});
await knex.schema.createTable('vet_specialties', (table) => {
table.integer('vet_id').notNullable();
table.integer('specialty_id').notNullable();
table.primary(['vet_id', 'specialty_id'], 'pk_vet_specialties');
table.foreign('vet_id', 'fk_vet_specialties_vets').references('id').inTable('vets');
table.foreign('specialty_id', 'fk_vet_specialties_specialties').references('id').inTable('specialties');
});
};
exports.down = async function down(knex) {
await knex.schema.dropTableIfExists('vet_specialties');
await knex.schema.dropTableIfExists('specialties');
await knex.schema.dropTableIfExists('vets');
};
/** Visits a pet paid to the clinic. */
exports.up = async function up(knex) {
await knex.schema.createTable('visits', (table) => {
table.integer('id').notNullable();
table.integer('pet_id').notNullable();
table.date('visit_date');
table.string('description', 255);
table.primary(['id'], 'pk_visits');
table.foreign('pet_id', 'fk_visits_pets').references('id').inTable('pets');
});
};
exports.down = async function down(knex) {
await knex.schema.dropTableIfExists('visits');
};
Every variant of the sample produces these seven tables. This diagram was drawn from the Flyway variant on PostgreSQL 18; on MySQL the types read INT and VARCHAR, on Oracle the names are upper case.
One multipart request queues the drawing; poll the job until it succeeds and download the SVG. Or let the GitHub Action do it and open a pull request with the diagram.
curl -X POST https://schemadrawer.com/api/v1/drawing \
-H "Request-Id: $(uuidgen)" \
-F "userId=5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f" \
-F "migrationType=KNEX" \
-F "databaseType=POSTGRESQL" \
-F "version=18" \
-F "files=@migrations/20240101000001_create_owners_and_pets.js" \
-F "files=@migrations/20240101000002_create_vets.js" \
-F "files=@migrations/20240101000003_create_visits.js"
# {"jobId":"0b6e2f4c-8d1a-4c3e-9f7a-2a5b6c7d8e9f"}
Then poll GET https://schemadrawer.com/api/v1/drawing/{jobId} until status is SUCCESS and fetch diagramUrl. Details in the getting started guide and the API reference.
- uses: schemadrawercom/action@v1
with:
sql-directory: migrations
output-path: docs/schema-diagram.svg
migration-type: KNEX
database-type: POSTGRESQL
database-version: '18'
github-token: ${{ secrets.GITHUB_TOKEN }}
The action uploads the directory, waits for the diagram and opens a pull request with the SVG. See the action reference.
knex_migrations and knex_migrations_lock are left out of the diagram.How five Node.js tools write, track and run database migrations, with a real migration file for each, a comparison table and advice on which one fits your project.
Editing applied migrations, environment-specific scripts, ORM auto-DDL in production, untested rollbacks: ten habits that break deployments, with a fix for each.
How Diesel manages a schema in Rust: the CLI, timestamped folders with up.sql and down.sql, the versions table, the generated schema.rs and embedded migrations.
Upload the migration directory, get an SVG back. Free while we build it.