MasterRecord

Migrations

Evolve your schema from code — diffed, versioned, reversible.

MasterRecord migrations compare your entity definitions against a saved snapshot, generate the SQL to reconcile them, and apply it in order. You get version-controlled schema changes with up/down support across SQLite, MySQL, and PostgreSQL.

Name the context file after the class
The CLI resolves a context by file name — enable-migrations AppContext looks for *AppContext.js. Keep your context in app/models/AppContext.js.

The workflow#

terminal
$ # 1. one-time: create the snapshot
$ masterrecord enable-migrations AppContext
$
$ # 2. generate a migration from current models
$ masterrecord add-migration InitialCreate AppContext
$
$ # 3. apply pending migrations
$ masterrecord update-database AppContext
$
$ # roll back the most recent migration
$ masterrecord update-database-down AppContext

Inside a Master app, use the friendlier wrapper — it targets AppContext, sets NODE_ENV (--env), and runs enable-migrations for you the first time, so master db new just works on a fresh app (as dotnet ef migrations add does):

terminal
$ master db new InitialCreate
$ master db migrate
$ master db status
$ master db rollback

What a migration looks like#

Generated migrations are plain ESM with up() and down() methods built on the schema API. Review and edit them before applying:

1700000000000_InitialCreate_migration.js
import masterrecord from 'masterrecord';

class InitialCreate extends masterrecord.schema {
  constructor(context) { super(context); }

  async up(table) {
    await this.init(table);
    await this.createTable(table.User);
    await this.createTable(table.Post);
  }

  async down(table) {
    await this.init(table);
    await this.dropTable(table.Post);
    await this.dropTable(table.User);
  }
}

export default InitialCreate;

Evolving the schema#

Change a model — add a field, an index, or a relationship — then generate another migration. MasterRecord diffs against the snapshot and emits just the delta (add column, create index, etc.).

terminal
$ master db new AddViewsToPost
$ master db migrate

Migration tracking#

Applied migrations are recorded in a _masterrecord_migrations table (EF’s __EFMigrationsHistory), so update-databaseruns only what’s pending and is safe to re-run (idempotent). It applies every pending migration in timestamp order — not just the latest file — and each migration commits atomically with its tracking row on Postgres and SQLite (one transaction per migration, as EF Core does), so a failure never leaves a half-applied schema. MySQL DDL implicitly commits, so a migration is not atomic there (EF documents the same). The snapshot records the latestMigration id, so two branches that each add a migration conflict on merge instead of silently diverging.

What the snapshot is#

<ctx>_contextSnapShot.json is the model as of the newest migration file— not the state of any database. It is written when a migration is authored (add-migration, enable-migrations, remove-migration) and never when one is applied, exactly as dotnet ef migrations add regenerates ModelSnapshot while dotnet ef database update leaves it alone. That is what makes each migration a delta between authored states, and it is why a brand-new database can replay them all in order. Commit it with your migrations.

Never create schema outside a migration
A table made by a bootstrap script or a hand-run CREATE TABLE is invisible to the snapshot, so later migrations build on schema nothing creates — they pass where you authored them and fail on a fresh database. Author a catch-up migration and masterrecord baseline <ctx> --all the databases that already have it. See Troubleshooting.

Review, status, remove#

terminal
$ # applied / pending / missing files (EF: migrations list)
$ masterrecord migrations-status AppContext
$ # print the SQL update-database WOULD run, without applying (EF: migrations script)
$ masterrecord script AppContext -o review.sql
$ # delete the latest migration file; --force reverts it first if applied (EF: migrations remove)
$ masterrecord remove-migration AppContext --force
$ # override the env-file connection for one run (EF: --connection)
$ masterrecord update-database AppContext --connection '{"type":"sqlite","connection":"./tmp/"}'

Migrating every context at once#

update-database-all applies pending migrations for every context in the project in one command. It has the same guarantees as update-database per context (runs all pending, records each in _masterrecord_migrations), isolates each context’s database connection, prints a per-context summary of what was applied, and exits non-zero if any context fails — so a deploy or CI step can detect a partial failure instead of it passing silently.

Note
Migrations describe schema (DDL) — create/alter/drop tables and columns. For data backfills, write raw SQL (e.g. this._execute(...)) inside the migration’s up() and test the run against a copy of your dev database first, since a backfill is not automatically idempotent the way the generated DDL is.

CLI command reference#

master dbmasterrecordDoesEF Core
enableenable-migrationsCreate the initial snapshot.
new <Name>add-migration <Name>Diff models → migration file.migrations add
migrateupdate-databaseApply all pending migrations (one context).database update
update-database-allApply pending migrations for every context.--context "*"
rollbackupdate-database-downRevert the most recently applied migration.database update <prev>
remove [--force]remove-migrationDelete the latest migration file (revert first with --force).migrations remove
statusmigrations-statusApplied vs pending.migrations list
script [-o file]scriptSQL for pending migrations, nothing applied.migrations script
listget-migrationsList migration files.
ensureensure-databaseCreate the database if missing (MySQL / Postgres).

Every command, alias and option — including --connection, update-database-target and the *-all variants — is on the CLI reference.