# Changelog 0.4

## v0.4.0 — 2026-10-06

### ✨ 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.
