Migrate commands
okm generate, migrate plan/apply/status/resolve/baseline/check, push, and seed.
generate
okm generate plans the schema against the last *.catalog.json in name order and writes SQL. The name defaults to migration. Letters, digits, _, and - are accepted. Anything else is Migration name <name> must be letters, digits, _ or -.
The file is <number>_<name>.sql in the migrations directory, with <number>_<name>.catalog.json beside it. The number is one higher than the highest numeric prefix, at least four digits. An empty directory starts at 0001. The name init also writes .gitattributes in the project when that file is missing.
An empty plan writes no SQL file and prints no changes. The out catalog is still written. A written file prints its path, then one line per lint finding. An error finding still writes the file, then exits 1; stderr says okm migrate apply will refuse until it is fixed. A warning does not change the exit code. migrate plan and check also exit 1 on an error finding.
--replace records a removed picklist or enum value. The schema stores no replacement. A chain, a value replaced by another removed value, is OKM1541. =null on a column that is not nullable is OKM1541.
bunx okm generate initbunx okm generate rename-status --replace notes.status.draft=openmigrate plan
okm migrate plan <name> prints the plan and writes nothing. A missing name is okm migrate plan needs a name.
With one target, or with --target, a database that answers adds pg_class row estimates to the lock lines. No target, several targets and no --target, or a target that cannot be reached, prints the plan without estimates. An unknown --target and a protection mismatch still throw. Those are the same refusals as Database. A pooler host is not refused.
An error finding is OKM1510 and the command exits 1 after the plan text. --json prints one envelope and no SQL text. data is name, class, operationClass, fromCatalogHash, toCatalogHash, planHash, requiresAck, destructiveOperations, each step's class, operationClass, lock, and sql, and the findings (severity, code, place, reason, fix). lock is the human -- lock: line. reason is an untrusted object. planHash covers the dialect, the two catalog hashes, and each step's SQL as executed. requiresAck is true when operationClass is destructive. Apply does not read it. The command still exits 1 after that document when a finding is an error. --replace is the same flag as generate.
bunx okm migrate plan initmigrate apply
okm migrate apply applies pending files in the migrations directory. A file without its .catalog.json is refused. With no migration files, or no head snapshot, it prints target <name> and nothing to apply and does not connect.
A pending file is linted after okm_history is read and before any DDL. lintFrom is not read. An error finding is OKM1510. Transaction control is the finding OKM1551. Apply accepts that step when it carries -- okm-allow OKM1551: reason. An empty reason, or a code the statement did not trigger, is OKM1510. migrate check still refuses the step.
Apply and push refuse OKM1899 before any step when okm_meta, okm_history, okm_backfill, or okm_steps already exists in the catalog schema and is not that ledger. The relation is not an ordinary table, or a column the ledger writes is missing. The message names the schema and the table and says another table owns the name.
The summary is target <name>, then set role <role> when apply issued SET ROLE, then restamped <id> for a history repair, then applied <id> for each file. Nothing applied is nothing to apply. A backfill batch is printed as it commits:
backfill <migration> step <index> batch <n> rows <n> key <last>key is the committed boundary, or - when that batch has none.
A protected target allows expand, reference rows, and provision of an empty database. Contract, unclassified SQL, a backfill, and a history repair that changes a row are OKM1850 until --allow-protected. An empty database, a database that already has objects and no history, and a provision that stopped part-way are on Database. Reference rows are on Reference rows. Timeouts are on Timeouts. Batch size, pause, and the batch statement timeout are on Backfill. Roles are on Roles.
A schema module that does not export a schema() still lets apply run. It installs no reference rows. The message is on Schema.
bunx okm migrate apply --target prodmigrate status
okm migrate status prints every configured target, in name order. --target keeps one name. Several targets do not need --target. No target is OKM1845.
The columns are tab-separated: target, version, catalog, state, protected. protected is true or false. The state is current, behind by expand, behind by contract, ahead by expand, ahead by contract, or failed at step <n> (resume with okm migrate apply).
A committed step that is in flight or broken follows as its own section: a line steps, then the tab-separated header id, kind, state, attempt, next. id is <migration>:<step> with a 0-based step index.
An unfinished backfill follows as its own section: a line backfill, then the tab-separated header migration, step, rows, key, state.
A pooler host is not refused. The command is read-only, including on a protected target.
bunx okm migrate status--json keeps that text in data.text and adds data.steps (id, kind, state, attempt, nextAction). id, kind, and state are untrusted objects read from okm_steps. The array is empty when no committed step is in flight.
migrate resolve
okm migrate resolve <migration>:<step> records a committed step a person finished, or marks it so the next apply tries it. The operand is the 0-based id from okm migrate status. The command needs an okm_steps row for that step. Pass one of the two actions.
Recording the step as applied writes the history row and deletes the okm_steps row. Where Postgres can probe the kind, or the step declares -- probe:, a probe that is not applied is OKM1554. Where no probe exists, the command is OKM1555 until --accept-unverified is passed. That flag has no short form. A declared probe is a probe, so that step does not need it.
Marking the step to run again sets the row to retry. The next apply probes first where a probe exists, and otherwise runs the statement.
There is no skip. A protected target is a history repair and needs --allow-protected. A pooler host is refused the same way apply refuses one.
bunx okm migrate resolve 0001_init:2 --retrybunx okm migrate resolve 0001_init:2 --applied--json puts target, step, action (applied or retry), and result (recorded or retry) in data.
migrate baseline
okm migrate baseline records the head snapshot as applied history on an existing database without running schema changes. Use it after okm pull and okm generate when the live database already matches the model. The guide is Existing database.
It introspects the target and plans managed objects against the head catalog. A difference is OKM1858 and prints that plan; nothing is marked applied. Objects the model does not name are listed as unmanaged and are left alone. A target that already has OKModel history is OKM1857. A repeat with the same baselined@<migration id> and catalog hash prints already baselined and changes nothing. The write creates okm_meta and okm_history when absent and inserts only those rows. The operation is history repair, so a protected target needs --allow-protected.
bunx okm migrate baselinebunx okm migrate baseline --dry-run --jsonbunx okm migrate baseline --allow-protected --target production--json puts target, migrationId, catalogHash, status (baselined, already-baselined, or dry-run), and unmanaged (kind, name) in data. name is an untrusted object.
baseline(target) from okmodel/migrate is the programmatic entry. default is the database target.
migrate check
okm migrate check replays every migration into a scratch schema inside one transaction, then rolls that transaction back and drops the schema. The schema name starts with okm_check_. An extension is database-wide. extension(name, { schema }) installs into that schema. The check creates the schema inside the replay transaction when it is missing, and the comparison keeps that schema instead of the scratch schema. A CREATE EXTENSION the connected role cannot run is OKM1811 before the replay. The message names the role and the extension: an untrusted default version needs a superuser, and a trusted one needs CREATE on the database. When the extension is already installed, the replay does not run CREATE EXTENSION again and never alters it: statements about it update a record kept in the check, the scratch schema reads its types through search_path, and a DROP EXTENSION fails when a scratch object still uses it. A column dropped earlier in that same step is not still using it. A step that ends the transaction therefore cannot change the installed extension. A btree index expression is reprinted on a temporary table before the catalog comparison, so a spelling Postgres does not store is not OKM1547. A using tail is not reprinted. A uuidv7() default on PostgreSQL 15, 16, or 17 is OKM1812 before any statement, with the fix uuidv7() needs PostgreSQL 18 or higher. minPostgres: 18. PostgreSQL 18 replays that default. A passing run prints ok <n> migrations, then one line: the replay uses empty tables, so a step that fails only on real data is not caught (NOT NULL on a populated column, a unique index over duplicates, a check over existing rows, a foreign key validation, or a cast). A statement left unrun follows on its own line, <id> step <n>: <reason>. The reasons are VACUUM is not run inside the check transaction, CREATE or DROP DATABASE is not run inside the check transaction, and ALTER SYSTEM is not run inside the check transaction.
A protected target is OKM1850. The message is okm migrate check writes a scratch schema and refuses protected target <name> (<reason>). Point it at a throwaway Postgres. --allow-protected does not lift it.
After a passing run the command lists every -- okm-allow line in the migrations directory, as allow <migration> step <n> <code> <class>, and marks destructive rules. --json data carries the list as allows[]. A reason shorter than 12 characters, a stop-list word such as ok or todo, or only the code repeated is warning OKM2300. It is an audit, so nothing is refused.
The command also checks the previous catalog, the head, and the linter. The first linted id is lintFrom. An error finding is OKM1510. Transaction control (begin, commit, rollback, abort, end, start transaction, prepare transaction), including inside a DO body, is that finding, OKM1551. A function or procedure body is not this. When the finding is allowed, the replay still throws OKM1551: Migration <id> step <n> contains <word>.
--provision also installs the head snapshot and compares it with a full replay. A difference is OKM1521: Provisioning from the snapshot does not match the replayed history.
timeouts apply when the config sets them. The flags do not. A lock timeout's fix says to run okm migrate check again when that lock is free. The step does not need a change. A pooler host is refused until allowPooler or --allow-pooler.
bunx okm migrate check --provisionpush
okm push plans the schema against the live catalog on the target and applies that plan as one migration named push-<n>, where n comes from okm_history. It writes no SQL file. An empty plan that already matches okm_meta prints target <name> and no changes. When the hash moved with no DDL, it re-stamps okm_meta and prints restamped push-<n> then nothing to apply. A plan with steps uses the apply summary.
Push runs no migration lint, with one exception. A plan that would weaken row-level security (OKM1550: disable or stop forcing it, drop or widen a policy, grant BYPASSRLS, or change the owner of a tenant table) is refused before any statement. The refusal is an OKM1510 error that lists the OKM1550 findings. Run okm generate, review the migration, then okm migrate apply it with -- okm-allow OKM1550: <reason> on the step.
A plan that drops columns and tables that hold rows is OKM1898. The check is select 1 from <table> limit 1 inside the push transaction, before any statement. The refusal names every object and --allow-data-loss. That flag applies the plan and prints dropped <object> for each one. An empty table, a rename, an added object, and a dropped constraint are applied. Dropping an empty column or table without the flag applies and prints nothing extra. A drop that .okm/pull.json already lists stays OKM1897. A reserved column or index name that the record does not list is OKM1122, the same error okm check reports. A ledger name that already belongs to another table is OKM1899, before any step. The message names the schema and the table.
A protected target is OKM1850 until --allow-protected. Lock and statement timeouts stay 5000 and 30000 milliseconds. The config and the flags are not applied. A pooler host is refused until allowPooler or --allow-pooler.
bunx okm pushseed
okm seed <file> loads that module and calls its default export with the testing harness for the selected target. The path is resolved from the project directory. A missing file is Seed file <file> was not found. A default export that is not a function is <file> must default-export a function. A missing file argument is okm seed needs a file.
The target is already migrated. The harness does not apply the catalog. A protected target is OKM1850 until --allow-protected. Several targets need --target. A pooler host is refused until allowPooler or --allow-pooler.
The command prints target <name> and one line <table> <count> for each table the factories inserted, in table name order. No insert is created nothing.
Under rlsTenancy the command runs as the application role. The owner, a superuser, or a BYPASSRLS role is OKM1707.
import type { Testing } from "okmodel/testing";
export default async function seed(t: Testing): Promise<void> {
const f = t.factories({
notes: (x) => ({ title: x.words(2) }),
});
await f.notes.create();
}bunx okm seed seed.ts