# Changelog 0.9

## v0.9.1 — 2026-10-17

### 💥 Breaking Changes

- Breaking-ish: now refused — a backslash in `fragment` / `raw` template text is OKM1125. Pass the value as an interpolation (B-01, D287).

- Breaking-ish: now refused — an unknown key on `insert`, `update`, upsert / `onConflict`, or first-or-create is OKM1120 and names the key and the table (B-39, D291).

- Breaking-ish: now refused — a finished statement with more than 65,535 parameters is OKM1126 (count and limit in the message) instead of a retryable `unavailable` (B-40, D292).

- Breaking-ish: now refused — an unknown CLI flag is an error; `okm <command> --help` lists that command's flags (B-26, D289).

- Breaking-ish: now refused — an `update` that would set a tree parent to the row itself or to a descendant is OKM1120 when `where` is the primary key (B-10, D295).

### 🔒 Security

- A backslash in a composed fragment can no longer desync the literal guard and escape a tenant filter (B-01, D287).

- A unique, foreign-key, or check failure on a table with a sensitive column no longer puts the value in `error.cause` (`detail`, parameters, `where`, and `internalQuery` stay off). Tables without sensitive columns keep today's cause. `includeValues` remains the explicit opt-in (B-33, D291).

- With `defineConfig({ roles })`, managed functions revoke `EXECUTE` from `PUBLIC` and grant it only to the app role, so the reporting role cannot execute them (B-31, D293). Provider-specific role checks (RDS, Cloud SQL, Supabase) follow in M-4.

### 🐛 Fixed

#### dialects

- `filters()` OKM1123 uses the calling table's own columns for allow/sort, and replaces per-table hidden marks when the same SQL name is redefined, so a prior `.hidden()` no longer poisons later suites that reuse `tasks.title` (and the same pattern for relation targets).

#### runtime

- A statement with more than 65,535 parameters fails with OKM1126 (count and limit in the message) instead of a retryable `unavailable`. The limit is for the whole statement, including filters and `inList` (B-40, D292). Split from the write-safety work on #339 when the combined change grew the featureless app by more than 100 B.

- An `update` that would set a tree parent to the row itself or to a descendant is OKM1120 when `where` is the primary key (B-10, D295).

- A throw from a custom `routing.select` keeps an `OkmError`'s kind; a plain throw becomes kind `invalid` instead of `driver` (B-35, D295).

#### adapters

- Bun.sql no longer parks a reserved handle for the next checkout, so concurrent scoped units after a backend kill keep separate sessions and tenant settings (B-03, D288).

- A reset or release failure after a statement or `COMMIT` completed destroys the connection and does not fail the call on Bun.sql, node-postgres, or Neon, so a committed write is reported as committed and `afterCommit` still runs (B-04, D288).

- node-postgres maps `Client has encountered a connection error and is not queryable` to a lost connection (`CONNECTION_CLOSED`), matching Bun.sql's message-only closed texts (D288).

- Bun.sql `reserve()` stamps code-less closed-socket texts (`Connection closed`, `connection must be a PostgresSQLConnection`) with SQLSTATE `08006`, so a checkout that fails before the reserved-handle guard classifies as `unavailable` instead of `driver` (B-03, D288).

- postgres.js caps reconnect backoff under 1 s with jitter, defaults `timeouts.acquire` to 10 s (OKM1846), swallows `nextWrite` on a null socket (Bun and Node), and rebuilds the pool so a kill storm's first OK read arrives within 3 s (B-02, B-43, D294). Other uncaught exceptions still print and leave a non-zero exit so `okm migrate check` / `apply` refusals are not reported as success on Bun.

- node-postgres defaults `timeouts.acquire` to 10 s and keeps an idle/checked-out client `error` listener that logs on the notice path without throwing, so a backend kill of an idle client does not crash Node (B-44, D294).

- A node-postgres call with `timeout` or `signal` removes its checkout `error` listener, so repeated timed calls do not stack listeners on a pooled client. A call that reaches its deadline while waiting for a connection is refused and does not run. A cancel that does not settle destroys that client, so the pool can serve the next call after a kill storm.

#### tooling

- CI copies the milestone from `Closes #N` onto the pull request before `lint / pull request`, so agents that cannot PATCH milestones still pass D206.

- The sticky migrate-plan comment matches the marker and the bot author, so another author's marked comment is not edited (B-22, D295).

- `okm driver test` drops leftover `okm_*` tables after each wire and error case, refuses Bun below `1.4.0`, and never reports an unrun case as `pass` (B-05, B-06, B-42, D295).

- `okmodel/testing` factories and `isolation()` work under `compositeTenancy()`; concurrent creates no longer false-positive as a factory cycle; matviews are refreshed in `isolation()` and documented otherwise (B-32, B-28, B-34, D295).

- The Bun.sql kill-storm test (B-03) retries post-storm scoped reads on `unavailable`/closed (bounded) so a dying reserved handle after the storm does not fail CI, without relaxing storm `short`/`leaks`/`readOnly`/`nested` asserts.

- `okm push` plans from the live catalog and applies each change under a fresh `push-<n>` id, so a second push is no longer skipped (B-29, D289).

- When the schema hash moves with no DDL (an external table the dependent already verified), `okm push` and `okm migrate apply` re-stamp `okm_meta` instead of leaving `connect()` on OKM1520 (B-21, D289).

- `okm migrate check` keeps a trigger's function in the logical namespace, so schemas with `timestamps({ enforce: "trigger" })` or hand-written triggers no longer fail OKM1020 (B-24, D289).

- `okm check` and `okm push` reprint domain checks on the connected server before planning, so a non-canonical author check is not permanent live drift (B-36, D289).

- The guide's `roles` block applies as a superuser and as a `CREATEROLE` schema owner: managed roles are created before `SET ROLE`, schema ownership is transferred when possible, and a failed role unit stays re-runnable with a clear privilege error. On Postgres 15, apply grants itself membership after `CREATE ROLE` (which leaves none); on Postgres 16+ it grants `WITH SET TRUE` (B-30, D293).

- Migrate strips the `pgbouncer` query parameter before the URL becomes a startup parameter; other pooler hints are not interpreted (B-25, D293). Provider-specific role-admin checks (RDS, Cloud SQL, Supabase) follow in M-4.

- The pooler pid probe uses eight reads on the apply session (OKM1854 when the backend pid changes). Warm-and-hold for LIFO PgBouncer is kept out of apply/seed so single-connection targets do not hang; URL heuristics still refuse typical pooler hosts and ports (B-41, D293).

- The type-change recipe's temporary `IS NOT NULL` validate is no longer OKM1531 (B-37, D289).

- An unknown CLI flag is an error; `okm <command> --help` lists that command's flags (B-26, D289).

- Backfill progress prints a 1-based step index (B-27, D289).

- `okm pull` and `okm import drizzle` write foreign keys with the exported table binding, keep file stems unique on case-insensitive volumes, and never write a Windows-reserved stem (`con`, `aux`, `nul`, `com1`, …). A column `__proto__` is a computed key. Composite primary-key columns stay not null on Drizzle import. Missing `oxfmt` prints a notice and still writes the report. `okm ext scaffold` includes composite types (B-14 to B-19, B-38, D290).

#### docs

- Tree, testing, driver-test, and Action docs cover cycle refusal, composite factories, minimum Bun, cleanup, and sticky-comment author matching (D295).

- Known limits record that Bun.sql does not park reserved handles, that a post-commit reset failure does not fail the call, and that a send-without-result is OKM1401 with no `afterCommit` (D288).

- `okm push` docs say it applies the live schema diff as `push-<n>`, and known-limits cover external re-stamp and domain-check sealing (D289).

- Sensitive docs cover redaction in logs, errors, `cause`, and spans; writes docs cover unknown keys (D291).

- Queries and the error reference document OKM1126 when a finished statement exceeds 65,535 parameters (D292).

- Guide roles privileges, production.md `--target` / status / check behavior, and known-limits pooler notes (DM-2, DM-3, D293).

- Known limits, README, guide, spec, and handbook pages match the code after the 0.9.1 fixes: Temporal polyfill on Node 22/24, `okm dev`, `okmodel/safety`, redaction, untyped `raw` and raw projection (B-07, B-08), minimum Bun, `okmodel.config.ts`, `listen` as a driver capability only, catalog export vs `describe`, backfill step numbering, and a Not tested section (DM-1 to DM-12, D296).

## v0.9.0 — 2026-10-16

### ✨ Added

#### dialects

- `okmodel/pg/unaccent`, `hstore`, `ltree`, `btree_gist`, `btree_gin`, `fuzzystrmatch`, and `pgcrypto` are generated extension packs. A feature the declared version does not include fails with OKM1812 and names `minPostgres`. `vector` is not in this version (D281).

#### tooling

- `describe(schema)` and `buildCatalog(description)` on `okmodel/migrate` return the catalog as JSON. The logical identity stays `public` when the catalog is placed. Reading the JSON back keeps the catalog hash (D284).

- `okm import drizzle <path>` writes table files from a Drizzle schema using the project's own `drizzle-orm` and the same emitter as `okm pull`. It needs no database. An existing file is OKM1855. A missing `drizzle-orm` is OKM1856. Composite foreign keys, `relations()`, and custom types are listed as not imported (D280).

- The GitHub Action `.github/actions/migrate` posts one sticky pull-request comment from `okm migrate plan --json`: the strictest class, each step's class and lock, row estimates, and lint codes. Preview applies a database the caller created and never creates or drops one (D279).

- `okm pull` reads a database and writes one table file per table plus the schema file. It is read-only, including on a protected target, and an existing file is OKM1855. Functions, triggers, views, policies, roles, and grants are listed under `not pulled yet` (D275).

- `okm migrate check` warns OKM1552 when a domain check or a `LANGUAGE sql` function body reprints, and OKM1538 warns only when the type change rewrites the heap (D276).

- `external(table)` marks a table another catalog owns. `okm catalog export` writes a snapshot whose `catalogHash` is separate from its logical identity. `defineConfig({ namespace, external })` places the catalog and verifies those snapshots. Two catalogs on one database keep separate history. A foreign key into another database is OKM1842 (D277).

- `okm pull` writes functions, triggers, views, and materialized views. A view is SQL with its columns. Views and `BEGIN ATOMIC` bodies take `dependsOn` from `pg_depend`. A plpgsql or SQL-string body gets lexical `dependsOn` candidates. A scratch replay with `check_function_bodies` on marks each function verified or candidate-only (D278).

- `okm ext scaffold <name>` prints a starting `extension()` definition from the connected catalog. `--out` writes that file and never overwrites. It is read-only, including on a protected target (D281).

- `okm ext test <file>` checks declared types, operators, functions, and index methods against the server. A wrong signature exits non-zero with the mismatch listed. A version the server does not meet is OKM1812. It is read-only, including on a protected target (D281).

#### docs

- A type change that rewrites the heap stays OKM1538, a warning with no automatic safe form. The documented recipe adds the new column, backfills it, dual-writes, switches reads, then drops the old column, each as its own migration (D285).

### 🐛 Fixed

- `btreeGin().gin` uses the `int4_ops` operator class that `btree_gin` creates.

- A `BEGIN ATOMIC` reprint is matched by the function name and its identity arguments, so two overloads do not share a body. A name that appears once is that row, so a typmod or an array is still reprinted. `orderBy` on a function call and computed fields stay after M2 (D282).

- The release smoke keeps asking npm while a published version is still processing: 31 times, 30 seconds apart, with `bun add --no-cache`. The job timeout is 45 minutes.

- `connect()` reads `okm_meta` and `okm_history` from the namespace `defineConfig` placed, so a second catalog's hash is checked at startup. `public` keeps the same query. A missing table still skips the check (D283).

### ♻️ Changed

- The execution plan schedules the 0.9 Tooling train as P110 through P119.

- 0.9.0 is the M2 gate. No gate or ceiling moved. `vector`, tool 5, `orderBy` on a function call, and computed fields stay deferred (D286).
