0.6

Every published OKModel release, newest first.

all series ยท 0.7 ยท 0.5

v0.6.0

๐Ÿ’ฅ 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.