v0.6.0
๐ฅ Breaking Changes
- Generated unique and foreign-key names use every SQL column after
casingandsqlName. 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 changescatalogHash. Upgrade withokm migrate applyonce. The plan isRENAME CONSTRAINTand does not drop the object or the rows. A protected target needs--allow-protectedon that invocation.ALTER INDEX โฆ RENAMEisSHARE 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 throughconnect()still reasons the stored name key. The column-tenancy fixture hash is nowdf7f8549fea5cde8e35a21e84541900abfe30fd605f4af0e7e98792f66aab32b. The small tasks schema isd029ad2cc216ec1b89062614a881d81841e7dbb1fc33602f9fd33203a1bc8461(D234, supersedes the TypeScript-name formula in D223). columnTenancy()takes one key. A key list iscompositeTenancy().tenancy: { via: "..." }is not a strategy. Usevia()fromokmodel/tenancy. A handwritten object is rejected when the schema is built (D229, pre-v1).filters({ allow })accepts only the operatorsparseapplies.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()runsREFRESH MATERIALIZED VIEWon a materialized view, withCONCURRENTLYwhen the view declares it. A plain view has norefresh()in its type. The statement is a write, and it is not tenant-scoped (D227).filters().parseappliesnot,containedBy,overlaps,hasKey, andhasAnyKey, and arelationsallowlist becomeshas. The allow type isFilterOperator(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 setsapp.tenantfor that transaction, and a view over those tables issecurity_invoker. A superuser, aBYPASSRLSrole, or the table owner is refused on the first scoped call (D231).
tooling
roles.reportingis 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 unlessroles.columnsnames it. The role is notBYPASSRLS.roles.columnsgrants select or update on named columns.withOwner(table(...), "ignored" | "external")keeps a table out of the plan. Apply grants the application role select onokm_metaand 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, messagetimeout) when a positivetimeoutis passed. They do not cancel a statement that is already running.timeout: 0is still kindtimeout. Asignalon its own is unchanged (D224, D239). - A
timeoutorsignalstill 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.tenantis 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
whereororderByis 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
whereandorderBycheck was measured in three forms. The smallest reusesfail()and fits under the current gates: the featureless app would be 90,718 / 30,143 (D225).
tooling
- A pull request labelled
needs: postgresruns the Postgres suite as four shards per version, each with its own database, and proves the pooler beside those shards.gate / checkwaits 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.unscopedand read every tenant through the select policy. It still cannot write (D238). okm seedunderrlsTenancyruns as the application role. The owner gets OKM1707.isolation()andtesting()on PGlite or a superuser harness are the same error. The supported path is a non-superuser role on a real Postgres throughopen({ url })withmigrate: false(D239).UNIQUE (id, tenant_id)stays besidePRIMARY KEY (id, tenant_id). Two tenants can share an id. Composite foreign keys andonConflicttarget 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
timeoutrefusal 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, andnot_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 untilokm 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.jsonandcatalog.hashare checked after a UTF-8 BOM is stripped and every carriage return is removed. An LF catalog keeps the hashokm buildwrote (D237).- A postgres.js connect that does not finish (
CONNECT_TIMEOUT) is kindunavailable.
dialects
okm checksees row-level security that was turned off. Introspection omits policies whilerelrowsecurityis 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 inroles.reportingand inroles.columnsselect and update (D241). rlsTenancystores the policy text Postgres prints. A lowercase unreserved column stays bare, so a snake-casetenant_idcatalog hash does not change.tenantId,TenantId, and a reserved word are double-quoted. Apply of a default-casing key no longer fails withcolumn "tenantid" does not exist, andokm checkreports no drift (D236).- A materialized view no longer keeps the application role's table default privileges. The plan revokes
INSERT,UPDATE, andDELETEafterSELECT. The catalog stays select only (D236). - A superuser passes the managed-role
CREATEROLEpreflight whenrolcreateroleis 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 setsapp.unscopedon a reserved primary connection, so the owner sees rows underFORCE ROW LEVEL SECURITY. The application role isnot_owner, and the error names the migration role. A plain view has norefreshat runtime.refresh: "blocking"and any other value is OKM1120 (D239).okm buildkeepst.jsonb<{ published?: boolean }>()andt.json<T>()in.okm/types.d.ts. A baret.jsonb()staysjsonb<unknown>(D215, D239).
adapters
- postgres.js does not
reforunrefa socket that is not writable. A closed socket no longer stacksconnectlisteners, so Node does not printMaxListenersExceededWarning. 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, andisolation()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.tenantbefore the statement, so one scope cannot return the other's rows (D241). tx({ timeout })andbatch({ timeout })on Bun.sql and PGlite are OKM1120, the same refusal as a per-call timeout. Asignalis 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 beforeCOMMIT. The pending statement and the transaction reject withunavailable, or OKM1401 when aCOMMITmay have happened, and the next call still runs. A lost reserved connection is not written again, so the driver never schedules the null-socketsetImmediatewrite, and the processsetImmediatestays the one the application installed. A node-postgres or Bun.sql stream interrupted by a lost connection isunavailable. 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
LISTENconnection 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 seedunderrlsTenancyruns 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)orwith check (true), grantsBYPASSRLS, or changes the owner of a tenant table is OKM1550.okm migrate checkand 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 checklints before it replays. The replay creates its scratch schema inside one transaction, sets the search path there, and always ends withROLLBACK. 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, andALTER SYSTEMare skipped and named. A concurrent index runs withoutCONCURRENTLY. A plain owner passes check (D242).- A step that kills its own backend during
okm migrate checkorokm migrate applyexits non-zero withunavailableor 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 FUNCTIONandCREATE PROCEDUREmay containCOMMITorROLLBACK. Top-level transaction control and aDObody stay refused. - Reprinting a view during
okm migrate checkruns as the login user.SET ROLEdoes not stay in effect for that schema, so a migration role can ownpublicand 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 generatethen 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 SCHEMAthat does not land fails with OKM1825. The migration role has to own the schema (ALTER SCHEMA โฆ OWNER TO).okm migrate checkgives that role its scratch schema and replays the grant there (D236).withOwner(..., "ignored" | "external")is omitted from generated SQL and from provisioning.okm checkstill ignores both (D236).- A CRLF or lone-CR migration file is read as LF. Apply,
okm migrate check, the linter, andokm migrate statussee the same steps. A non-empty file with no steps and no-- no stepsline fails instead of printingnothing to apply. A hand-writtentruncateis 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 initwrites.gitattributesso migration SQL, catalog JSON, and.okm/stay LF. An existing.gitattributesis left as it is (D237).okmon Node older than 22.18 stops and namesbun --bun okm.engines.nodeis>=22.18(D237).okm migrate checkkeeps 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 migration0004_sql_namesrenames the constraints whose SQL names changed (D234).- A
CREATE FUNCTIONorCREATE PROCEDUREwith a SQL-standardBEGIN ATOMICbody stays one statement. Semicolons in the body do not split it, and the closingENDis not OKM1551. A nestedCASE ... ENDdoes not close the body. Top-level transaction control, aDObody, andBEGIN ATOMICoutside those statements stay refused. - The Postgres workflow caches with
actions/cachev5.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 checkshould run as a non-superuser. A superuser replay can executeCOPY ... PROGRAMandlo_import.CREATEROLEis 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
MaxListenersExceededWarningafter 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.