# Changelog 0.6

## v0.6.0 — 2026-10-11

### 💥 Breaking Changes

- Generated unique and foreign-key names use every SQL column after `casing` and `sqlName`. A primary key stays `{table}_pkey`. A picklist check stays the SQL column. A named check keeps the author's name. Indexes are unchanged. A catalog with a multi-column unique or foreign key changes `catalogHash`. Upgrade with `okm migrate apply` once. The plan is `RENAME CONSTRAINT` and does not drop the object or the rows. A protected target needs `--allow-protected` on that invocation. `ALTER INDEX … RENAME` is `SHARE UPDATE EXCLUSIVE`. A database created before this spelling plans only those renames, and the next plan is empty. `connect()` on the old catalog is OKM1520 until apply. A violation through `connect()` still reasons the stored name key. The column-tenancy fixture hash is now `df7f8549fea5cde8e35a21e84541900abfe30fd605f4af0e7e98792f66aab32b`. The small tasks schema is `d029ad2cc216ec1b89062614a881d81841e7dbb1fc33602f9fd33203a1bc8461` (D234, supersedes the TypeScript-name formula in D223).

- `columnTenancy()` takes one key. A key list is `compositeTenancy()`. `tenancy: { via: "..." }` is not a strategy. Use `via()` from `okmodel/tenancy`. A handwritten object is rejected when the schema is built (D229, pre-v1).

- `filters({ allow })` accepts only the operators `parse` applies. `path`, `matches`, `similar`, `wordSimilar`, `or`, `and`, `none`, `every`, and the write operators are a type error (D233, pre-v1).

### ✨ Added

#### dialects

- `db.views.name.refresh()` runs `REFRESH MATERIALIZED VIEW` on a materialized view, with `CONCURRENTLY` when the view declares it. A plain view has no `refresh()` in its type. The statement is a write, and it is not tenant-scoped (D227).

- `filters().parse` applies `not`, `containedBy`, `overlaps`, `hasKey`, and `hasAnyKey`, and a `relations` allowlist becomes `has`. The allow type is `FilterOperator` (D233).

#### runtime

- `compositeTenancy({ key: ["organizationId", "workspaceId"], type: "uuid" })` isolates on every key together. `tenancy: via("project.organization")` isolates a table through up to three relations. `for()` takes every key. A column-only import does not load either factory (D228, D229).

- `rlsTenancy({ key: "tenantId", type: "uuid" })` adds PostgreSQL row-level security behind the column predicate. Tenant tables get forced policies, each scoped call sets `app.tenant` for that transaction, and a view over those tables is `security_invoker`. A superuser, a `BYPASSRLS` role, or the table owner is refused on the first scoped call (D231).

#### tooling

- `roles.reporting` is a read-only role: select on tables, views, and materialized views, usage on schemas, and default select on later tables. A hidden column stays out of that select unless `roles.columns` names it. The role is not `BYPASSRLS`. `roles.columns` grants select or update on named columns. `withOwner(table(...), "ignored" | "external")` keeps a table out of the plan. Apply grants the application role select on `okm_meta` and no other privilege there (D232).

- `isolation()` reads tenant views and materialized views after seeding the underlying tables. A global view is skipped and listed (D233).

#### docs

- The startup budget audit for train 0.6 records where the bytes in each gated graph go, the headroom under the current gates, and the measured cost of the smallest hook for each 0.6 feature. It does not move a gate (D218).

- The docs site in `site/` publishes the handbook from the repository markdown, with search, per-page markdown, and a changelog page. The home page links install, the first query, schema, and migrations.

- Learning pages for install, the first query, schema, queries, safety, migrations, and drivers are written in the site handbook. Repository docs stay under Reference. A public fence that names a shipped API is type-checked against the built package.

### ♻️ Changed

#### dialects

- The extra unique beside a tenant primary key keeps the stored name key `{primary fields}_{tenant field}`. Its SQL name is the column list (D234, supersedes the TypeScript spelling in D223).

#### adapters

- Bun.sql and PGlite throw OKM1120 (kind `invalid`, message `timeout`) when a positive `timeout` is passed. They do not cancel a statement that is already running. `timeout: 0` is still kind `timeout`. A `signal` on its own is unchanged (D224, D239).

- A `timeout` or `signal` still does not settle an in-flight statement on Bun.sql or PGlite. The fix grew Bun.sql by 108 / 39, so it was not shipped (D224).

#### runtime

- Inside `tx()`, `app.tenant` is set again when the statement's scope differs from the last one in that transaction. A rolled-back savepoint does not leave the transaction unscoped (D238).

- A where with no effective predicate is checked from the write and archive chunks. A read does not carry that walk. The runtime entry stays 5,288 / 2,026. The featureless app is 90,506 / 30,063 (D219).

- A hidden column named in `where` or `orderBy` is OKM1120. `hidden({ filterable: true })` allows those two clauses and still leaves the column out of the default row. `filters()` still refuses it (OKM1123). The catalog hash is unchanged. The featureless app is 90,721 / 30,154, under the 90,900 / 30,170 gate (D226).

- The hidden `where` and `orderBy` check was measured in three forms. The smallest reuses `fail()` and fits under the current gates: the featureless app would be 90,718 / 30,143 (D225).

#### tooling

- A pull request labelled `needs: postgres` runs the Postgres suite as four shards per version, each with its own database, and proves the pooler beside those shards. `gate / check` waits for the suite and the tarball. A skipped Postgres slice fails that gate while the label is present (D230).

#### docs

- The reporting role is not tenant-safe against its own SQL. It can set `app.unscoped` and read every tenant through the select policy. It still cannot write (D238).

- `okm seed` under `rlsTenancy` runs as the application role. The owner gets OKM1707. `isolation()` and `testing()` on PGlite or a superuser harness are the same error. The supported path is a non-superuser role on a real Postgres through `open({ url })` with `migrate: false` (D239).

- `UNIQUE (id, tenant_id)` stays beside `PRIMARY KEY (id, tenant_id)`. Two tenants can share an id. Composite foreign keys and `onConflict` target the unique. `archivable()` turns it into a partial unique index, which a primary key cannot be (D234, D239).

- Known limits put the Bun.sql and PGlite `timeout` refusal on the first screen. A populated materialized view is not RLS-protected (D239).

- The plan schedules M2 Depth as four trains: 0.6 Access (P80–P86), then 0.7 Query depth, 0.8 SQL lane, and 0.9 Tooling. Access is first. Each feature loads lazily, or the step stops with numbers. The OKE prototype starts after 0.6 (D217).

- Known limits name the driver reasons `undefined_table`, `undefined_column`, `tls`, `not_populated`, `no_unique_index`, and `not_owner`. The handbook covers composite, path, and row-level tenancy, the reporting role, and the constraint-name upgrade from 0.5.1. A production database that is behind the app is OKM1520 until `okm migrate apply`.

- The 0.6 gate remeasured the six startup graphs. Minified and gzip bytes match the previous sample, so the gates stay. The featureless app has 138 minified and 31 gzip left under 90,900 / 30,170. The same ten tables with column tenancy are 97,735 / 32,468 (D235).

### 🐛 Fixed

#### contracts

- `.okm/catalog.json` and `catalog.hash` are checked after a UTF-8 BOM is stripped and every carriage return is removed. An LF catalog keeps the hash `okm build` wrote (D237).

- A postgres.js connect that does not finish (`CONNECT_TIMEOUT`) is kind `unavailable`.

#### dialects

- `okm check` sees row-level security that was turned off. Introspection omits policies while `relrowsecurity` is false, so the plan enables and forces them again (D238).

- A column grant or revoke lowercases only the privilege keyword. `tenantId`, `Mixed Case`, a reserved word, and a snake_case column stay quoted byte for byte in `roles.reporting` and in `roles.columns` select and update (D241).

- `rlsTenancy` stores the policy text Postgres prints. A lowercase unreserved column stays bare, so a snake-case `tenant_id` catalog hash does not change. `tenantId`, `TenantId`, and a reserved word are double-quoted. Apply of a default-casing key no longer fails with `column "tenantid" does not exist`, and `okm check` reports no drift (D236).

- A materialized view no longer keeps the application role's table default privileges. The plan revokes `INSERT`, `UPDATE`, and `DELETE` after `SELECT`. The catalog stays select only (D236).

- A superuser passes the managed-role `CREATEROLE` preflight when `rolcreaterole` is false (D236).

- `refresh()` waits until it is awaited. `batch([view.refresh()])` is OKM1121 and runs no statement. An unawaited failure is not an unhandled rejection. The refresh sets `app.unscoped` on a reserved primary connection, so the owner sees rows under `FORCE ROW LEVEL SECURITY`. The application role is `not_owner`, and the error names the migration role. A plain view has no `refresh` at runtime. `refresh: "blocking"` and any other value is OKM1120 (D239).

- `okm build` keeps `t.jsonb<{ published?: boolean }>()` and `t.json<T>()` in `.okm/types.d.ts`. A bare `t.jsonb()` stays `jsonb<unknown>` (D215, D239).

#### adapters

- postgres.js does not `ref` or `unref` a socket that is not writable. A closed socket no longer stacks `connect` listeners, so Node does not print `MaxListenersExceededWarning`. A connect that does not finish rejects the statement.

#### runtime

- The first scoped statement on each endpoint checks the role and the policy expressions. OKM1707 names that endpoint. A policy changed to `using (true)` fails the check, and `isolation()` fails with it (D238).

- A policy that is missing, not forced, or not the one okm emits is OKM1708. The guard resolves the table with `to_regclass`, so a schema named after the connected role does not hide it. The check still runs once per endpoint and generation (D241).

- Statements of different scopes on one transaction connection run one at a time. Each sets its own `app.tenant` before the statement, so one scope cannot return the other's rows (D241).

- `tx({ timeout })` and `batch({ timeout })` on Bun.sql and PGlite are OKM1120, the same refusal as a per-call timeout. A `signal` is not refused (D241).

- postgres.js no longer crashes the process when the backend dies inside `tx()`, including while the transaction is idle between statements, inside a savepoint, or just before `COMMIT`. The pending statement and the transaction reject with `unavailable`, or OKM1401 when a `COMMIT` may have happened, and the next call still runs. A lost reserved connection is not written again, so the driver never schedules the null-socket `setImmediate` write, and the process `setImmediate` stays the one the application installed. A node-postgres or Bun.sql stream interrupted by a lost connection is `unavailable`. A postgres.js stream can still end a page early with no error (D242).

- A node-postgres idle client that the server closes no longer crashes the process. The pool drops that client and the next query opens a new one. A `LISTEN` connection that the server closes is the same: one error handler covers the pool and that client, the client is released, and a later query still runs. The connect entry measures 42,661 minified and 15,066 gzip. The gate stays 42,850 / 15,070.

- OKM1707 says `okm seed` under `rlsTenancy` runs as the application role. The owner gets this error (D239).

- A trailing backslash on the catalog directory is ignored, and a Windows stack frame still records `file.ts:line` (D237).

- `onConflict: { on: "title" }` on a tenant unique of (tenant key, title) updates that tenant's row. The conflict target includes the tenant key. Naming the tenant key is still OKM1104. Two tenants can keep the same title (D221).

#### tooling

- A hand-written step that disables row-level security, stops forcing it, drops a policy, widens one with `using (true)` or `with check (true)`, grants `BYPASSRLS`, or changes the owner of a tenant table is OKM1550. `okm migrate check` and apply refuse it unless the step has `-- okm-allow OKM1550: reason` (D238).

- OKM1550 also flags a create or alter policy on a tenant table that is not the emitted expression: `using (1=1)`, `using ((true))`, `using (true or false)`, `alter policy … to public using (1=1)`, and an extra permissive policy (D241).

- `okm migrate check` lints before it replays. The replay creates its scratch schema inside one transaction, sets the search path there, and always ends with `ROLLBACK`. It does not edit system catalogs or real ACLs. `COMMIT`, `ROLLBACK`, and other transaction control in a step is OKM1551. Apply accepts that step only with `-- okm-allow OKM1551: reason`. `VACUUM`, `CREATE DATABASE`, `DROP DATABASE`, and `ALTER SYSTEM` are skipped and named. A concurrent index runs without `CONCURRENTLY`. A plain owner passes check (D242).

- A step that kills its own backend during `okm migrate check` or `okm migrate apply` exits non-zero with `unavailable` or OKM1401. The process does not hang. Check drops the scratch schema, or the error names it when the drop cannot be confirmed.

- OKM1551 reads a dollar-quoted body only when the statement is `DO`. `CREATE FUNCTION` and `CREATE PROCEDURE` may contain `COMMIT` or `ROLLBACK`. Top-level transaction control and a `DO` body stay refused.

- Reprinting a view during `okm migrate check` runs as the login user. `SET ROLE` does not stay in effect for that schema, so a migration role can own `public` and the check still creates the reprint schema (D242).

- A path foreign key is planned after the unique it references. Table names no longer decide that order. Drops reverse it (D236).

- `okm generate` then apply accepts an index on a materialized view. The stored document depends on the view, not on a column, for one column or several, unique or not (D236).

- `GRANT USAGE ON SCHEMA` that does not land fails with OKM1825. The migration role has to own the schema (`ALTER SCHEMA … OWNER TO`). `okm migrate check` gives that role its scratch schema and replays the grant there (D236).

- `withOwner(..., "ignored" | "external")` is omitted from generated SQL and from provisioning. `okm check` still ignores both (D236).

- A CRLF or lone-CR migration file is read as LF. Apply, `okm migrate check`, the linter, and `okm migrate status` see the same steps. A non-empty file with no steps and no `-- no steps` line fails instead of printing `nothing to apply`. A hand-written `truncate` is OKM1542 in LF and in CRLF (D237).

- A UTF-8 BOM is stripped from migration SQL. UTF-16 is refused. The kind stays `invalid` (D237).

- `schema`, `tables`, `migrations`, `out`, and a seed path resolve from the project directory. An absolute path and a `..` path both work (D237).

- `okm generate init` writes `.gitattributes` so migration SQL, catalog JSON, and `.okm/` stay LF. An existing `.gitattributes` is left as it is (D237).

- `okm` on Node older than 22.18 stops and names `bun --bun okm`. `engines.node` is `>=22.18` (D237).

- `okm migrate check` keeps a constraint whose only difference is the stored name key. Postgres does not store that key, so a replay no longer drops and recreates it. The reference app migration `0004_sql_names` renames the constraints whose SQL names changed (D234).

- A `CREATE FUNCTION` or `CREATE PROCEDURE` with a SQL-standard `BEGIN ATOMIC` body stays one statement. Semicolons in the body do not split it, and the closing `END` is not OKM1551. A nested `CASE ... END` does not close the body. Top-level transaction control, a `DO` body, and `BEGIN ATOMIC` outside those statements stay refused.

- The Postgres workflow caches with `actions/cache` v5.1.0, which runs on Node.js 24.

#### docs

- Known limits and the migrations page describe a Windows checkout: CRLF, a BOM, resolved paths, a TCP URL, and case-sensitive file names (D237).

- `okm migrate check` should run as a non-superuser. A superuser replay can execute `COPY ... PROGRAM` and `lo_import`. `CREATEROLE` is not required for the replay.

- Known limits record the postgres.js stream gap: a lost connection can end a page early with no error, and a later `cursor.next()` on a dead socket can throw outside the promise. node-postgres or Bun.sql is the stream to use when failover matters. The 0.7 backlog reclaims adapter and featureless-app bytes before adding that loop (D242).

- The handbook names draft 26, and the opening size table is the current sample.

- Known limits and the drivers page no longer say postgres.js prints `MaxListenersExceededWarning` after a lost connection.

### 🔒 Security

- An update that sets a path column checks the new parent in that same statement. A parent in another tenant, a missing parent, or a parent deleted while the statement waits is OKM1705, and the row stays. `batch()` refuses that update before it runs. `tx()` is the verified form.

- `archivable()` on the table a path ends on is refused when the schema is built (OKM1705). The fix names that table. A path child can still be archived.
