Strata · guide

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.

Install it
pnpm add @niscorp/strata zod @electric-sql/pglite
Step 1 of 5

A 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.

A database one migration behind
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] });
Step 2 of 5

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.

Ask for the plan
const plan = await status(pool, [theatre]);
console.log('pending:', plan.pending.map((migration) => migration.ref + ' ' + migration.description));
console.log('problems:', plan.problems.length);
Step 3 of 5

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.

Rehearse the run
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);
Step 4 of 5

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.

Refuse to start with anything pending
try {
  await migrate(pool, [theatre], { mode: 'verify' });
} catch (error) {
  if (!(error instanceof StrataError)) throw error;
  console.log('verify:', error.code);
}
Step 5 of 5

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.

A migration edited after it ran
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]);
}
Then

Run it.

The one pending migration, the rehearsal that leaves it pending, the code verify stops with, and the migration Strata says was edited.

Run it
npx tsx plan.mts
It prints
pending: [ 'theatre.app/3 Season passes' ]
problems: 0
would apply: [ 'theatre.app/3' ]
still pending: 1
verify: PENDING
EDITED: theatre.app/2 ("Bookings")