Read the plan before you run it
What a run would do, asked three ways without doing it, and what Strata says when a migration was edited after it ran.
pnpm add @niscorp/strata zod @electric-sql/pgliteA database one migration behind
The same theatre as before. The database has run the first two migrations, and the code now has three.
Strata does not ask for a driver. It asks for two functions: query, and transaction, which it needs because a run lands whole or not at all. A pg pool is wrapped the same way.
import { PGlite } from '@electric-sql/pglite';
import { StrataError, defineSequence, type Migration } from '@niscorp/strata';
import { migrate, status, type StrataPool } from '@niscorp/strata/postgres';
const db = new PGlite();
const pool: StrataPool = {
query: (text, values) => db.query<Record<string, unknown>>(text, values),
transaction: (run) => db.transaction((tx) => run({ query: (text, values) => tx.query(text, values) })),
};
const shows: Migration = {
description: 'Shows',
steps: [{ kind: 'sql', sql: 'CREATE TABLE shows (id text PRIMARY KEY, title text NOT NULL)' }],
};
const bookings: Migration = {
description: 'Bookings',
steps: [{ kind: 'sql', sql: 'CREATE TABLE bookings (id text PRIMARY KEY, seats integer NOT NULL)' }],
};
const passes: Migration = {
description: 'Season passes',
steps: [{ kind: 'sql', sql: 'CREATE TABLE passes (member_id text PRIMARY KEY, season text NOT NULL)' }],
};
await migrate(pool, [defineSequence({ id: 'theatre.app', migrations: [shows, bookings] })]);
const theatre = defineSequence({ id: 'theatre.app', migrations: [shows, bookings, passes] });Ask for the plan
status reads the ledger and answers with the plan: what is pending, in the order it would run, and every problem. It writes nothing.
const plan = await status(pool, [theatre]);
console.log('pending:', plan.pending.map((migration) => migration.ref + ' ' + migration.description));
console.log('problems:', plan.problems.length);Rehearse the run
dryRun goes further: it runs the pending steps for real, inside the transaction, and then rolls the transaction back. A statement that would fail fails here. Afterwards the same migration is still pending.
const rehearsal = await migrate(pool, [theatre], { dryRun: true });
console.log('would apply:', rehearsal.applied.map((migration) => migration.ref));
console.log('still pending:', (await status(pool, [theatre])).pending.length);Refuse to start with anything pending
In verify mode nothing is applied, and anything pending is an error. This is for a server that should start only against a database somebody has already migrated.
try {
await migrate(pool, [theatre], { mode: 'verify' });
} catch (error) {
if (!(error instanceof StrataError)) throw error;
console.log('verify:', error.code);
}A migration edited after it ran
Somebody adds a column by changing the SQL of the second migration. This database ran the old SQL, so its checksum in the ledger no longer matches the code.
Strata reports EDITED and will not run against the database, because nobody can say any more what it holds. The way out is to put the migration back and append the column as a new one.
const edited: Migration = {
description: 'Bookings',
steps: [{ kind: 'sql', sql: 'CREATE TABLE bookings (id text PRIMARY KEY, seats integer NOT NULL, paid boolean)' }],
};
const changed = defineSequence({ id: 'theatre.app', migrations: [shows, edited, passes] });
for (const problem of (await status(pool, [changed])).problems) {
console.log(problem.code + ':', problem.detail.split(' was applied')[0]);
}Run it.
The one pending migration, the rehearsal that leaves it pending, the code verify stops with, and the migration Strata says was edited.
npx tsx plan.mtspending: [ 'theatre.app/3 Season passes' ]
problems: 0
would apply: [ 'theatre.app/3' ]
still pending: 1
verify: PENDING
EDITED: theatre.app/2 ("Bookings")