# Changelog 0.5

## v0.5.1 — 2026-10-08

### ♻️ Changed

- A pull request no longer runs the Postgres suite. The label `needs: postgres` runs the suite on 15 and 18 and the tarball on 18, in the same Actions run as the other checks. Adding that label without a push starts it on its own and does not cancel the run. `bun run verify` runs that suite on this machine. The release and the weekly run still cover 15 through 18 (D212).

- A pull request update is one Actions run. Checks are named `area / piece`, so each one reads `CI / area / piece`. Long test groups are split (`migrate` and `migrate pg`, `drivers` and `rows`, `schema`, `columns`, and `schema pg`, `tenancy`, `client`, and `client pg`, `package`, `contracts`, and `safety`). Node and Deno are `runtimes / portable`. Path labels and the pull request lint are jobs in that run. The required status is `gate / check` (D213).

- Running the Postgres suite with no database is loud. The preflight names the unreachable primary and how many files would skip, and `CI=true` fails instead of running. Each skipped Postgres test counts toward a final `N Postgres tests SKIPPED` line (QA-R2, D216).

- CI runs the date/time codecs on Node 22 and 24 with `temporal-polyfill` and on Node 26 natively, in a `temporal / node` job that joins the `gate / check` needs. The Postgres suite workflow proves the D215 pid probe refuses a transaction-mode pgbouncer with OKM1854 on every suite run (QA-R3, D216).

- `bun run type-cost` measures a 250-table inferred schema beside the other probes and fails above measured +20% (21,180 instantiations). Check time is reported, not gated (QA-R4, D216).

### 🐛 Fixed

#### runtime

- `update`, `delete`, `archive`, and `restore` now refuse a where with no effective predicate, including undefined values (QA-C1, QA-H2). A field value is a leaf. `eq` keeps a timestamp, a date, bytes, or a jsonb object in the filter. A bare object stays OKM1121 (D125, D210).

- An `or()` branch with no effective predicate is now refused, in reads and in writes (QA-H1). `or([])` still matches nothing.

- `or()` with a non-array or with more than one argument is now refused (QA-M4).

- An unknown key on `update`, and `tx` with the function before the options, are now refused (QA-M2).

- A bad option on `find`, `one`, and `count` now rejects the query, so `safe` and `catch` see OKM1120 (QA-M11). `page` and `aggregate` already did.

- A read on a replica that refuses or drops the connection falls back to the primary on postgres.js, node-postgres, and Bun.sql. `onRoute` reports `fallback:unhealthy`. A driver error with an empty message now has text (QA-M1, D214).

- Closing a client before it connects no longer leaves an unhandled rejection. `connected` rejects with an `unavailable` error that says the client was closed first (QA-M5, D214).

- OKM1104 lists each accepted constraint once and does not name the tenant key, which every unique includes (QA-L11, D214).

- OKM1200 reads `Validation failed: <field> <reason>` and names the first failing field (QA-L12, D214).

#### dialects

- Compiled SQL for a tenant table keeps the tenant predicate on every table alias, including a where that compares an object through `eq` (QA-S4).

- A missing table or column maps to `undefined_table` or `undefined_column`, with a fix that names `okm migrate apply` and `okm migrate status`. Calls that share one caller signal now share one abort listener instead of adding one each (QA-L8, D214).

- A TLS or certificate failure maps to `unavailable` with reason `tls` and a fix that names `sslmode` (QA-L9, D214).

- `filters().parse` no longer throws a TypeError on `constructor`, `__proto__`, `toString`, or `hasOwnProperty`. A key that is not allowlisted is OKM1123. Parse applies `eq`, `in`, `notIn`, `lt`, `lte`, `gt`, `gte`, `between`, `startsWith`, `endsWith`, `contains`, `like`, and `ilike`. An operator the allow type accepts and parse cannot apply is OKM1120 when `parse()` runs (QA-M9, D215).

- The shipped `Temporal` declaration is empty interfaces, so it merges with TypeScript's Temporal lib and with `temporal-polyfill` (QA-L2, D215).

- Emitted row types keep `jsonb<T>` and write an optional insert column as `x?: T` (QA-L3, D215).

- `fn({ searchPath })` is checked as a comma-separated list of identifiers when the function is built (QA-S3, D215).

#### tooling

- The commit-cut harness waits until the server has answered `COMMIT` before it drops the client. A fixed delay could reset the socket first, so the transaction rolled back while the caller still got `outcome_unknown`.

- A hand-written migration step is linted per statement. A leading comment, a second statement, a semicolon inside a comment, `DO` and `CALL`, and `ALTER COLUMN … TYPE` can now be refused (QA-H3). `-- okm-allow` on the step silences that code on every statement of the step.

- Port 6432 counts as a pooler. Apply, seed, and check refuse a backend pid that changes between two statements, with OKM1854, unless `--allow-pooler` is set (QA-M6, D215).

- `okm check` on a schema with `citext` and a view exits 0. The scratch search path includes the schemas that hold extension types, then restores the previous value (QA-M7, D215).

- `isolation()` uses a new tenant pair each run, asserts only on the row it inserted, deletes that row, and names a missing factory. It covers aggregate, page, batch, and stream when the client has them (QA-M8, D215).

- A CLI error prints `error <code>: <message>` and the fix, and exits 1. The stack is printed only with `--verbose` or `OKM_DEBUG=1` (QA-M10, D215).

- The published `package.json` no longer carries the `workspaces` field. The pack step strips it from the tarball only; the root keeps it, and the tarball smoke test asserts it (QA-R1, D216).

- Apply's `failed at step N` counts from 1, the same number the linter prints. Status still prints the stored index (QA-L5, D215).

- A renamed table's plan row estimate uses the old table's row count (QA-L6, D215).

- `okm`, `okm help`, `okm --help`, and `okm -h` print the commands and exit 0. `okm <command> --help` prints that command (QA-L7, D215).

- Apply refuses a database whose `standard_conforming_strings` is off (QA-S2, D215).

#### docs

- Temporal values keep microseconds. Showing one passes `fractionalSecondDigits`. The codecs do not warn (QA-L1, D215).

- `db.views` keys are camelCased. A materialized view is refreshed with `REFRESH MATERIALIZED VIEW` until a `refresh()` handle in 0.6 (QA-L10, D215).

- `lists_touch` is the trigger `timestamps({ enforce: "trigger" })` installs on a table named `lists`. `archivable()` installs no trigger (QA-L13, D215).

- Hidden columns in `where` and `orderBy` are unchanged. The options for 0.6 are recorded in D215 (QA-S1).

- `timeout` and `signal` on Bun.sql and PGlite do not settle a call whose statement is already running, and an outer-client call inside a PGlite `tx()` callback queues behind the transaction instead of throwing (QA-M3, D216). Neither driver can cancel an in-flight statement, and the client-side race did not fit the size cap, so both stay known limits. OKM1192 is reserved for the later refusal.

- Date and time columns need a `Temporal` global. Bun and Node 26 have it; Node 22 and 24 need the polyfill line before the columns are used (QA-D1, D216).

- The `okm` bin's shebang is node: `bunx okm` runs with Node, `bun --bun okm` selects the Bun runtime, and a `.ts` config needs Node 22.18 or newer (QA-D2, D216).

- The update/delete rule names its codes: no effective predicate, including undefined values, is OKM1102, and an empty `or()` branch is OKM1121 (QA-D3, D216).

- The linter page states the statement split, comment handling, and the `DO` and `CALL` rule (QA-D4, D216).

- Circuits start closed, so the first read goes to a replica without waiting for a probe (QA-D5, D216).

- Known limits records the M3 outcome, sub-microsecond rounding on write, the PGlite outer-client queue rule, and the thirteen `filters().parse` operators (QA-D6, D216).

- Production states the pooler refusal: the refused hosts and ports, the pid probe, and `--allow-pooler` (QA-D7, D216).

- The views section shows the camelCase handle and the `REFRESH MATERIALIZED VIEW` refresh (QA-D8, D216).

- Catalog identity and constraint names wait for 0.6, with the rename design (QA-L4).

## v0.5.0 — 2026-10-07

### ✨ Added

#### runtime

- `connect({ primary, replicas })` opens one pool per endpoint and probes each replica. A string or a pool is unchanged and does not load that code (D201).

- Reads on a topology go to an eligible replica. `route: "primary"` or `"replica"` forces a read. `db.using("primary" | "replica")` returns a client with no `close` and no `using`. `onRoute` reports the choice, and a throw from it is ignored. `routing.fallback` is `"primary"` or `"error"`. A required replica that is not eligible is OKM1843. An automatic read with `fallback: "error"` and no eligible replica is OKM1844. A write, transaction, or locking read on a replica scope is OKM1840. A string or pool client serves either `route` from its one endpoint and has no `using`. `inspect()` stays `single-endpoint`. Routing reasons in `inspect()` are the M2 dev inspector (D202).

- `routing.select` chooses the replica: `weighted` (the default, smooth weighted round-robin), `roundRobin`, `leastConnections`, `latencyAware`, or a function. A weight of 0 is OKM1120. A saturated replica is skipped on an automatic read. If every healthy replica is saturated, the read uses the primary (`fallback:saturated`) or OKM1844. `route: "replica"` still uses a saturated replica. A connection failure retries once with the same strategy (D203).

- `routing.consistency` is `"session"` (the default) or `"eventual"`. A committed write reads `pg_current_wal_insert_lsn()` before its promise resolves when replicas are configured. That is one extra round trip. Later reads use a replica that has replayed at least that far. The root, `for()`, `unscoped()`, `using()`, and `reserve()` share that watermark. A failed position read keeps automatic reads on the primary (`fallback:position-unknown`, or OKM1844) until the next successful read. A primary probe already in flight does not clear that state; only a probe started in the same unknown generation does. `route: "replica"` and `using("replica")` are OKM1843 while the position is unknown or no replica satisfies it. Healthy replicas that are behind report `fallback:behind` (D204).

- `routing.maxLag` accepts `"5s"`, `"500ms"`, `"2m"`, `"16MB"`, `"512KB"`, `"1GB"`, and `"4096B"`. A bare number or any other form is OKM1120. A caught-up replica has lag zero. A custom `select` receives that lag in bytes, or `null` when it is unknown (D204).

#### tooling

- A migrate target that carries `primary`, `replicas`, `weight`, or `pool` is OKM1845. The command does not resolve that URL.

- Issues and pull requests follow one GitHub standard: one `type:` label, at least one `area:` label, a `type(scope):` title, and a milestone. A check on the pull request adds the `area:` labels from changed paths, then enforces it. Dependabot opens grouped weekly `chore(deps)` updates, labelled, with no issue or milestone required (D206).

#### docs

- A reference app in `packages/reference-app` (private, not published): a multi-tenant project tracker on public entry points, with traits, archive, three generated migrations, a function, views, and the replica topology. CI runs it in the `postgres` and `tarball` jobs: use cases with query counts and `isolation()`, two preview databases at once from the snapshot, a rehearsal on a populated `TEMPLATE` clone, and read-your-write as the application role. `docs/example-app.md` describes it (D207).

- The README covers the 0.5 topology: `connect({ primary, replicas })`, automatic read routing, `route`, `db.using`, `routing.select`, consistency, `onRoute`, and OKM1840, OKM1843, OKM1844, OKM1845, and OKM1846, plus the reference app as the worked example. The session-watermark cost and the fallback rate under write load are recorded for a primary and two standbys (D209).

### ♻️ Changed

- An automatic read uses `routing.select` instead of the first healthy replica.

- The session wrote flag is gone. A read after a committed write follows the commit position.

### 🐛 Fixed

#### dialects

- View scratch verification creates the schema's functions before its views, so `okm check` passes for a view that calls a declared function (D208).

- `archivable()` writes the partial unique index predicate the way Postgres prints it, `(archived_at IS NULL)`, so a unique column on an archivable table no longer drifts after every apply. Stored catalogs change once (D208).

- A function grant names its argument types from `oidvectortypes`, so `EXECUTE` on a function with named arguments no longer drifts, and `okm doctor` and the apply preflight check that grant instead of skipping it (OKM1825) (D208).

- Grants and default privileges target the schema they are applied to. `okm migrate check --provision` with `roles` no longer grants on the target's own objects (D208).

#### runtime

- `db.views` is typed from `schema({ views })` on connected, scoped, and routed clients. Each field is the camel-cased column name, typed `string | null`. Types only (D208).

- Smooth weighted round-robin clears the current weight of a replica that did not take part in a pick. After that replica is eligible again, a heavier replica keeps the next read (D205).

#### tooling

- `okm migrate check` reads each scratch schema with that schema as the search path. A declared view and a function that takes a schema enum no longer fail with OKM1547 or OKM1020 (D208).

- `okm migrate check` with `roles` keeps default privileges in the scratch schema, so it passes and no longer writes them to the target's `public` (D208).

- Roles, grants, and default privileges are left out of the stored catalog hash and the `.okm` artifact. `connect()` after an apply with `roles` no longer fails with OKM1520 when `catalogDir` is not passed. `okm check` still verifies them. A catalog without roles hashes as before (D208).

- `okm migrate apply` moves a hash stored before D208 to the new hash when it is exactly the old hash of a migration already applied, also when nothing is pending, and prints `restamped <migration id>`. A protected target needs `--allow-protected`. Any other hash is left as drift (D208).

- Upgrade, archivable with a unique column: after upgrading, `connect()` is OKM1520 and `okm check` is OKM1510 (`OKM1529 creates a unique index`). Run `okm generate archive_predicate`, add `-- okm-allow OKM1529: rebuilds the same index` above the `create unique index concurrently` line, then `okm migrate apply`. `okm migrate check` still reports OKM1547 at the migration that first created the index, as it did before; see known limits (D208).

- Upgrade, a database applied with `roles`: after upgrading, `connect()` is OKM1520 until `okm migrate apply` runs once (`--allow-protected` on a protected target). It prints `restamped <migration id>` (D208).

- `okm migrate apply` on several databases of one cluster at once no longer fails when they create the same managed role. Every apply but the first failed on `pg_authid_rolname_index` with a raw driver error. `CREATE ROLE` now ignores a role another session created first, in the step runner and in the snapshot install, and later `ALTER ROLE` steps still run (D207).

- `isolation()` no longer fails with OKM1120 on a schema with a tenant-scoped view. It checks tables only; a view that hides the tenant key is still OKM1820 in `okm check` (D207).
