v0.4.0
✨ Added
runtime
- When the catalog hash differs,
connect()readsokm_historyand uses the stored class. Ahead by expand migrations succeeds. Ahead by a contract migration, or behind the app, is OKM1520. The message names the migration, and the fix says to apply it or change the deploy order.requireMetais unchanged (D197). - A
catalog.hashfile makes that mismatch load.okm/catalog.jsoninstead of rebuilding the catalog from the schema. Concurrent connects in one process parse the file once. A missing file, or a hash that does not match, is OKM1027. Equal hashes still return without that read.
tooling
okm migrate planprints apg_class.reltuplesestimate on each lock line when a selected target is reachable:about 4.2M rows,rows unknown (table not analyzed), ornew table. A safe rewrite is labelled on that line.ACCESS EXCLUSIVEon more than one million estimated rows, outside a safe rewrite, prints a note and is not a lint finding. No target, or a target that cannot be reached, prints the lock alone.okm generatestays offline, and the estimate is not written to SQL, catalog files, orokm_history(D194).- The migration linter flags a drop, a rename, a column type change, a required column with no default, a dropped default, a shorter length, and a new unique, check, or foreign key on an existing table as errors.
- On an existing table the planner emits the safe form: a concurrent index create and drop, a check or foreign key as
NOT VALIDthenVALIDATE,SET NOT NULLthrough a validated check, a volatile default split into a batched fill, and a unique or primary key from a concurrent unique index (D193). A new table keeps the plain statements. - A failed concurrent index leaves an invalid index. Resume drops it and rebuilds, and does not repeat a finished step.
okm generateprints findings and still writes the file.okm migrate planandokm checkexit non-zero on an error finding.okm migrate applylints migrations that are still pending on the target and refuses with OKM1510 before any DDL or data statement.- An override is
-- okm-allow OKM15xx: reasonon the line above the statement. An empty reason, or a code the statement did not trigger, is OKM1510 and does not silence another code. - A backfill step stays one idempotent
UPDATEin the migration file, with-- backfill table=… key=… batch=…. Apply runs it outside the migration transaction, one commit per batch, and resumes fromokm_backfill. A table with no primary key is OKM1546.defineConfig({ backfill })sets the batch size (1,000), the pause (none), and the statement timeout (30 seconds). A protected target still needs--allow-protected(D195). okm migrate statuslists an unfinished backfill under the target table: migration, step, rows so far, last key, and state.okm migrate checkreplays every migration into a scratch schema, plans each result back to that file's catalog, checks the previous catalog, requires the last catalog to match the schema, and lints the history (D196). Success printsok N migrations. A remaining step is OKM1547, including a fork (migration X was generated from a different parent than Y). An expand migration that drops something the previous catalog relies on is OKM1548. A stale head is OKM1549 and the fix isokm generate. A protected target is refused: point the command at a throwaway Postgres.--allow-protecteddoes not apply.defineConfig({ lintFrom: "<migration id>" })is the adoption baseline forokm migrate check. Files before that id are not linted. Apply still lints pending migrations only.okm migrate statusprintscurrent,behind by expand,behind by contract,ahead by expand,ahead by contract, orfailed at step N (resume with okm migrate apply). The command stays read-only on a protected target (D197).okm migrate applyon an empty target installs the head snapshot andreferencerows, recordsprovisioned@<migration id>, and does not replay steps. A non-empty target with no history is OKM1851. An empty protected target is allowed without--allow-protected(D198).table({ reference: { key, rows } })inserts a missing key on provision and on every apply. It does not update or delete. The rows stay on the schema and out of the catalog hash.provision(target)fromokmodel/migrateprovisions one configured target under the same rules.okm migrate check --provisioncompares that snapshot with a full replay. A difference is OKM1521 and names the object. The default check is unchanged.- OKM1542 flags
insert,update,delete,merge, andtruncateoutside a backfill step. Reference inserts are not migration steps, so they are not flagged. okmodel/testingexportstesting(schema, { driver }). It opens a real driver pool (open()fromokmodel/pg/pglite, oropen({ url })fromokmodel/pg/postgresjs) and returnsdb,factories,expectQueries,isolation, andclose. A factory fills a required column from its type, replays from an optional seed, andx.refreuses a row in the same tenant.expectQueriesfails with the statements that ran and does not count transaction control.isolation()runs each tenant table's basic queries under tenant A against a row of tenant B and names a leak (D199).okm seed <file>calls the module's default export with that harness on the selected target. A protected target is OKM1850 unless--allow-protected. Several targets need--target(OKM1853). The command prints the rows the factories inserted.
docs
okm migrate checkhas a CI recipe that starts Postgres and runs the command. The previous-catalog check is schema-level and does not prove application behaviour. Tenant targets and schema-per-tenant checking stay M5. Startup compatibility is theconnect()check in D197, not this command.- Provisioning and reference rows are documented, including where the rows live and what
--provisioncompares (D198). docs/testing.mddocuments factories,expectQueries,isolation(), andokm seed. Spec section 20 namesokmodel/testingas an export and usesopen()fromokmodel/pg/pglite.
♻️ Changed
- Drop default, a
NOT NULLcolumn with no default on an existing table, drop identity,SET GENERATED ALWAYS, andALTER ROLEare classified contract.okm migrate planprints each step's class and lock. - A removed picklist or enum value, and a volatile default on an existing table, are filled by that batched backfill. The final rows match the single
UPDATEthey replace.okm migrate planaddsabout N batcheswhen a row estimate is available. - OKM1534, OKM1535, OKM1536, and OKM1537 are errors, and they fire only when the statement is not the safe form. A type change that rewrites the table (OKM1538) stays a warning.
timestampwithout time zone,varchar(n),serial,json, and an identity that is not generated always stay warnings onokm check.
🐛 Fixed
tooling
okm checktreats a column default and the cast Postgres stores for that column's type as the same value.'x'matches'x'::texton a text column. A different value still plansSET DEFAULT. The catalog keeps the text that was written.okm checkalso ignores a length or precision Postgres omits from that cast.'x'::character varyingmatchesvarchar(10),'x'::bpcharmatcheschar(n), and atimestamp(3)cast without the precision matches the same literal. A cast to another type still plansSET DEFAULT.- A provision that stops after creating objects and before writing
provisioned@is OKM1851 on the next apply. The message names the object and says to drop the schema or database and run again. Apply does not replay over that target and does not delete it. Managed tables with no history row and no other objects are still empty. - A table
renamedFromplansALTER TABLE … RENAME TO, then renames a primary key, unique or check constraint, foreign key, index, or identity sequence when the name is the default{table}_…. A custom name stays. An add and a drop with no declaration is still OKM1530 (D194). - Two serializable transactions in the conformance suite no longer write at the same time. One commits first, so the other is the only victim and the retry count stays one.
okm checkcreates domain and enum types in its scratch schema before the stub tables, so a view over those columns can be sealed.- A view, materialized view, table, sequence, or function that a plan drops and creates is granted again afterwards. Postgres drops the grant with the object.
- Introspection reads a constraint
nameKeyfrom the default name, so a check declared asemailPresentstill matches the live database. bun run db:upbinds three free host ports when 55432, 55433, or 55434 is already taken, and records them for the next CI step. Release smoke no longer stops when the runner is already using 55432.