0.4

Every published OKModel release, newest first.

all series · 0.5 · 0.3

v0.4.0

✨ Added

runtime

  • When the catalog hash differs, connect() reads okm_history and 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. requireMeta is unchanged (D197).
  • A catalog.hash file makes that mismatch load .okm/catalog.json instead 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 plan prints a pg_class.reltuples estimate on each lock line when a selected target is reachable: about 4.2M rows, rows unknown (table not analyzed), or new table. A safe rewrite is labelled on that line. ACCESS EXCLUSIVE on 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 generate stays offline, and the estimate is not written to SQL, catalog files, or okm_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 VALID then VALIDATE, SET NOT NULL through 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 generate prints findings and still writes the file. okm migrate plan and okm check exit non-zero on an error finding. okm migrate apply lints migrations that are still pending on the target and refuses with OKM1510 before any DDL or data statement.
  • An override is -- okm-allow OKM15xx: reason on 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 UPDATE in the migration file, with -- backfill table=… key=… batch=…. Apply runs it outside the migration transaction, one commit per batch, and resumes from okm_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 status lists an unfinished backfill under the target table: migration, step, rows so far, last key, and state.
  • okm migrate check replays 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 prints ok 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 is okm generate. A protected target is refused: point the command at a throwaway Postgres. --allow-protected does not apply.
  • defineConfig({ lintFrom: "<migration id>" }) is the adoption baseline for okm migrate check. Files before that id are not linted. Apply still lints pending migrations only.
  • okm migrate status prints current, behind by expand, behind by contract, ahead by expand, ahead by contract, or failed at step N (resume with okm migrate apply). The command stays read-only on a protected target (D197).
  • okm migrate apply on an empty target installs the head snapshot and reference rows, records provisioned@<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) from okmodel/migrate provisions one configured target under the same rules.
  • okm migrate check --provision compares 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, and truncate outside a backfill step. Reference inserts are not migration steps, so they are not flagged.
  • okmodel/testing exports testing(schema, { driver }). It opens a real driver pool (open() from okmodel/pg/pglite, or open({ url }) from okmodel/pg/postgresjs) and returns db, factories, expectQueries, isolation, and close. A factory fills a required column from its type, replays from an optional seed, and x.ref reuses a row in the same tenant. expectQueries fails 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 check has 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 the connect() check in D197, not this command.
  • Provisioning and reference rows are documented, including where the rows live and what --provision compares (D198).
  • docs/testing.md documents factories, expectQueries, isolation(), and okm seed. Spec section 20 names okmodel/testing as an export and uses open() from okmodel/pg/pglite.

♻️ Changed

  • Drop default, a NOT NULL column with no default on an existing table, drop identity, SET GENERATED ALWAYS, and ALTER ROLE are classified contract. okm migrate plan prints 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 UPDATE they replace. okm migrate plan adds about N batches when 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. timestamp without time zone, varchar(n), serial, json, and an identity that is not generated always stay warnings on okm check.

🐛 Fixed

tooling

  • okm check treats a column default and the cast Postgres stores for that column's type as the same value. 'x' matches 'x'::text on a text column. A different value still plans SET DEFAULT. The catalog keeps the text that was written.
  • okm check also ignores a length or precision Postgres omits from that cast. 'x'::character varying matches varchar(10), 'x'::bpchar matches char(n), and a timestamp(3) cast without the precision matches the same literal. A cast to another type still plans SET 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 renamedFrom plans ALTER 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 check creates 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 nameKey from the default name, so a check declared as emailPresent still matches the live database.
  • bun run db:up binds 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.