Repository navigation
Releases: omqkhafi/okmodel
Releases · omqkhafi/okmodel
Release list
v0.10.2
Nothing changes in the catalog hash, the generated SQL, or the public API, apart from the stricter descendants() and ancestors() types below.
🐛 Fixed
- A value that a
.sensitive()or hidden column rejects no longer appears in the error. Reads now say onlyField <name> was rejected.(OKM1210) in the message,toHttp(),log(), and the cause, as writes already did. This covers awhereonfind,page,one,count, andexists, values insideinList, relation filters (has,none,every), and thewhereofupdateanddelete. An ordinary column still names the rejected value. distinctOnreports the default-limit warning (OKM2011) even after an earlierdistinctread. Each read that takes a default limit now reports it from its own call site.- The OKM1120 "Accepted names" list on a write names only the fields that write may set. Columns it would refuse with OKM1190 are left out: an
id()identity default, guarded columns (unlessallownames them), trait fields, and the primary key on an update. A primary key you set yourself stays in the list on an insert. descendants()andancestors()on atree()table now need an options argument with alimitin TypeScript whenconnectdoes not setlimits.find, the same asfind(). A baredescendants(id)used to compile and then throw OKM1101. Withlimits.findset, a bare id still works, and.all(reason)is still available.- On a Bun.sql pool from
open(), a query that the server terminated while it ran (for example withpg_terminate_backend) now fails with a lost-connection error. It used to run again on a new connection. Bun 1.4.4 made this happen every time. - When a seed file fails to import,
okm seednow names the seed file and the module that could not be loaded. - On the docs site, unknown pages return 404 instead of a server error, and the old
/docs/define/*and/docs/start/*links redirect to their new pages.
📚 Docs
- The error pages no longer claim that row values never appear in errors. They say when a value can appear in
toHttp()for an ordinary column, and that a table with any.sensitive()column keepsvalues()undefined and a reduced cause even witherrors.includeValues. - The seed docs explain that relative imports in a seed file use the
.tsextension underbunx okm, with an example that seeds a globaltenantstable and then tenant rows.
v0.10.1
💥 Breaking Changes
descendants()andancestors()on atree()table now need a row bound, the same asfind(). Passlimit, set a default withconnect({ limits: { find } }), or read every row with.all(reason). Without one the call throws OKM1101 (descendants() needs a limit.). Alimitabovelimits.maxis OKM1101 too.depthstill counts walk steps and is not a row limit.
♻️ Changed
- Idle pool connections now close after 30 seconds on postgres.js, node-postgres, Bun.sql, and Neon over WebSocket. Set
idleTimeoutMsonconnectoropento change it, oridleTimeoutMs: 0to keep idle connections open as before. A pool reconnects on the next query, including insidedb.with()andtx(). A short script that ends withoutawait db.close()can now wait for the idle timer before it exits; calldb.close()or setidleTimeoutMs: 0. Neon over HTTP refuses the option (OKM1120), and PGlite has no pool to time out.
✨ Added
- When
connect({ limits })setslimits.findorlimits.page,find()andpage()no longer requirelimitin TypeScript, so a barefind()compiles and uses that default. Each key works on its own, andmaxdoes not change the types. A client withoutlimits, includingtesting(), still requireslimit. okm initwriteslimits: { find: 100, include: 20, page: 50, groupBy: 500, max: 1000 }into a new generated client. When an existing client has no limits, it prints the snippet instead of editing the file.okm init --limitsadds them to a generated client you have not edited; an edited client is printed and left as it is.
🐛 Fixed
- A generated column is now ordered after the columns its expression reads.
testing().close()and the migration planner no longer fail with Postgres error2BP01when the generated column's name sorts before one of its sources. The catalog hash and existing migration files do not change. onQueryon adb.with()client reports one event per call. The settings prelude,BEGIN,COMMIT, andRESET ALLare no longer reported as separate queries.WriteEvent.settingslists the setting names, never the values, for writes through adb.with()client, includingtx(), typedraw(), archive, and restore. It used to be empty.- Under
for(), warning OKM2005 no longer fires whenorderByends on the rest of a unique key that starts with the pinned tenant key. distinctanddistinctOnreads honorlimits.findandlimits.max.- The OKM1120 list of accepted names on insert, update, upsert, and first-or-create no longer offers the tenant key, which those writes fill in. Reads still list it.
- OKM1709 says "an int" instead of "a int".
- When
process.cwd()throws, as on some edge runtimes, okmodel uses./.okm, the same as whenprocess.cwdis missing.
🔥 Removed
- Error code OKM1025. It was registered but never thrown: two
Registerdeclarations that giveschemadifferent types are already TypeScript error TS2717. The number stays unused.
📚 Docs
- The handbook at okm.omqkhafi.dev has a new structure: capability groups, a first-success path, Concepts, Frameworks pages with tested examples, task Guides, a split CLI reference, Handling errors, one Observability page, and new homepage examples.
- The Markdown pages in the package's
docs/folder are now generated from the handbook and the error registry.
v0.10.0
💥 Breaking Changes
DriverCapabilitiesno longer includespreparedordescribe. Prepared-statement mode stays a connect option. A driver that can describe a statement still exposespool.describe.hidden({ filterable })accepts only a literaltrue.falseand a boolean variable are type errors. Callhidden()with no argument. Passingfalsefrom JavaScript still leaves the column hidden and not filterable.okm migrate plan --jsonfindingreasonis an untrusted object.okm migrate baseline --jsonunmanagednameis an untrusted object.okm migrate statusJSONid,kind, andstateare untrusted objects. A database error's text iserrors[].detail.summarystays a registry sentence, orA database error was returned (<kind>).when the code has no page.db.batchof a versioned update throws OKM1121 before any statement; move that update intotx().tx.batchis unchanged, andversion: "any"stays allowed.- Bound parameters stay off an error cause unless
includeValuesis set. A page cursor for a hidden or sensitiveorderBycarries only the primary key (a gone row is OKM1130), andOkmErroris branded soinstanceofmatches across two copies of the class (#541).
behaviour changes
- Indexes on a tenant table lead with the tenant key (
indexes: true, the default). A unique index is unique per tenant, so two tenants may share a value.tenantIndexes: falseturns the prefix off for one table..global("reason")opts one index out (an empty reason is refused) and.scoped()opts one index in. The nearest level wins. An index that already leads with the key is unchanged, and the nextokm generatewrites nothing. An index that does not is replaced once:CREATE INDEX CONCURRENTLYfor the new index, thenDROP INDEX CONCURRENTLYfor the old one. That is not a blocking rebuild, andokm migrate checkaccepts it (Q3, D306, #390). OKM1706 stays when prefixing is off. - With
validation: true, an unknown key oninsert,update, upsert /onConflict, first-or-create,insert.validate,insert.check,insert.pick(...).validate,insert.pick(...).check,insert.omit(...).validate,insert.omit(...).check,update.validate, orinsert["~standard"].validateis OKM1120 and names the key and the table (R1, D304). A typo-onlyupdatenames the key instead of saying "update set is empty."insert.check,pick(...).check, andomit(...).checkreturn rule issues and do not throw OKM1200 for a failed rule. An unknown key throws OKM1120 on those calls too, and the rule issues are lost. The Standard Schemavalidatethrows that OKM1120. It does not return{ issues }for an unknown key. A failed rule on that surface still returns{ issues }. - On a
versioned()table, an update withversioninsetis OKM1120 (was OKM1190). A version string other than"any", such as"2", is OKM1120 (was OKM1121). The message says it must be an integer or"any". Insert ofversionstays OKM1190. okm generateexits 1 when an error finding exists; the migration file is still written; stderr saysokm migrate applywill refuse until it is fixed (Q8, #395). A warning-only finding stays exit 0.okm pullandokm import drizzlewrite relative imports with the.tsextension so Node type stripping andbunx okmcan load the schema (R4, #401).tscneedsallowImportingTsExtensionswithnoEmit, orrewriteRelativeImportExtensions.- An
updateon aversioned()table withoutversionis a TypeScript error; the runtime OKM1120 stays when types are bypassed (Q5, D305, #392). Non-versioned tables are unchanged. okmexit codes follow the outcome.0is done or nothing to do.1is a command that ran and failed.2is usage or configuration (unknown flag, missing config, a Node older than 22.18).3is a decision the command will not ask (OKM1530, and a committed step that needs a person: OKM1553, OKM1555).4is a guard (OKM1850, OKM1852, OKM1855, OKM1859).5is a target that cannot be reached or a refused feature (OKM1802, OKM1803, OKM1811, OKM1812, OKM1843, OKM1844, OKM1846, OKM1854, OKM1860, and a lost connection). Every other code, including OKM1401, planner OKM1541, OKM1554, OKM1556, OKM1851, OKM1857, and OKM1858, stays1. A script that only checked non-zero still fails. A script that treated every failure as1now sees2through5.okm migrate plan --json,okm migrate baseline --json, andokm catalog export --jsonkeep their previous fields inside theokm.cli/1envelopedata.parsePlanJsonreadsdataand still accepts a bare plan document.okm buildandokm devreject an unknown flag. They used to leave the rest of the line unread.- A derived name longer than 63 bytes (index, constraint, sequence, check, trigger) keeps the same shape, a UTF-8 prefix,
_, and 8 hex characters, but the suffix is now the 32-bit FNV-1a hash of the full name, not the first 8 characters of its SHA-256. The stored name of every such identifier changes once, so the nextokm generateplans a change for it. Names that fit in 63 bytes are unchanged. Two long names that shorten to the same stored name stay OKM1023 (duplicate identity) and are never merged. The featureless app startup graph drops 2,415 minified bytes becausesha256.tsleaves it (90,763 to 88,348). schema()resolves every cased name once:snakesplits an acronym (userIDisuser_id),casing: "camel"is accepted, any other casing is OKM1060, and two fields that spell one SQL name are OKM1023. A drop and add that are one casing of each other is OKM1530 and names.sqlName()and.renamedFrom(), and a column rename also renames its identity sequence (#535, #548).- Under composite tenancy,
index().unique()leads with every tenant key, an ordinary index still leads with the first, a key already named moves to the front once, and two indexes that become the same one are OKM1023.tenantIndexeson a global table, or a tenant-index mark when the schema has no tenancy, is OKM1060, and OKM1706 on ausingindex namesindex().global("reason")(#546). - A table named
capabilitiesis OKM1023, becausedb.capabilitiesis reserved on the root client, a scoped client, and insidetx()(#546). find,one, andpagereject a plain hidden key, an unknown key, a wrong filterable value, a bador()branch, and a hiddenorderBy, including insideinclude, andpagerejects an unknown option andrelevance()(OKM1120). The same plain hidden key and a bador()are type errors oncount,exists,update, anddelete, while a filterable hidden column stays valid inwhereandorderBy(#549, #550).
✨ Added
contracts
DescribeResultaddsparameters(type OIDs in$norder) andfields(name,typeOid,tableOid,attnum).columnsandparameterCountstay.ExecuteResult.reportedis the driver command-tag count, or null when the driver reported none. Absent on fixtures that only setcount. The caller-facingcountstaysreported ?? rows.length.
adapters
- postgres.js and PGlite
describereturn those fields and do not execute the statement. node-postgres, Bun.sql, and Neon still leavedescribeunset. resultFromrecords the command tag onExecuteResult.reported. postgres.js and Bun.sql usecount. pg and Neon userowCount. PGlite uses per-statementrowCount, and cumulativeaffectedRowsonly whenrowCountis absent. An own field that is not a finite number is nothing; a later field is not a fallback.
runtime
- A connected client exposes
capabilities, the same frozen object as the pool (transactions,stream,listen,cancel). update({ where, set, limit, lock: "skip", version: "any" })on aversioned()table skips the version check, bumpsversionby 1, and returns the claimed rows. A miss is an empty array.versionstays required on every other versioned update.version: "any"withoutlock: "skip"andlimit, an unrecognised version string, and aversionkey insetare OKM1120 (Q6, O-5, #393).connect({ onWrite })observes write units (okm.observe/1): buffered write, savepoint, and tri-state outcome events after commit or rollback, from one emission point shared withhookm.onTransaction. Normative shape indocs/observer-events.md.db.flush()waits for theokmodel/observehelper (app size gate).okmodel/observeexportswriteObserver(),flush(),OBSERVE_BUFFER_MAX(10,000), andobserverBufferOverflow()(OKM2200). The helper buffers per unit, calls exactly one ofcommitted|rolledBack|unknown, isolates throwing and rejecting handlers viaonObserverError, and settles tracked promises onflush(). Overflow drops the oldest unit without blocking the write path.writeObserver({ keys })names schema tables. Those writes append primary-keyRETURNINGwhen the statement is a simple insert, update, or delete, and put the values onWriteEvent.keys.sql()andinspect()show the extra clause.describe()labels the appended columns"observer". A table with no primary key, or a name missing from the schema, is OKM1890 at connect and at plan time. NestedRETURNING(parent guard, versioned close, archive) is not rewritten. A caller result is unchanged: observer-only rows are hidden.- Write events take
rowCountfrom the command tag. Mode ischangedfor insert, update, and delete,returnedwhenRETURNINGran, andunknownwithrowCount: nullwhen the driver gave no tag. An upsert stayschangedand counts inserted plus updated rows. find({ offset })skips that many rows. The value is an integer from 0 up and is sent as a parameter.okmodel/warningsexportswarnings(). Pass it toconnect({ warnings }). In development, OKM2004 (offset past 1,000), OKM2005 (a limit whose order does not end in a unique key), OKM2007 (a value list past 500), and OKM2008 (an include whose row bound passes 10,000) ...
v0.9.1
💥 Breaking Changes
- Breaking-ish: now refused — a backslash in
fragment/rawtemplate 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> --helplists that command's flags (B-26, D289). - Breaking-ish: now refused — an
updatethat would set a tree parent to the row itself or to a descendant is OKM1120 whenwhereis 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, andinternalQuerystay off). Tables without sensitive columns keep today's cause.includeValuesremains the explicit opt-in (B-33, D291). - With
defineConfig({ roles }), managed functions revokeEXECUTEfromPUBLICand 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 reusetasks.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 andinList(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
updatethat would set a tree parent to the row itself or to a descendant is OKM1120 whenwhereis the primary key (B-10, D295). - A throw from a custom
routing.selectkeeps anOkmError's kind; a plain throw becomes kindinvalidinstead ofdriver(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
COMMITcompleted destroys the connection and does not fail the call on Bun.sql, node-postgres, or Neon, so a committed write is reported as committed andafterCommitstill runs (B-04, D288). - node-postgres maps
Client has encountered a connection error and is not queryableto 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 SQLSTATE08006, so a checkout that fails before the reserved-handle guard classifies asunavailableinstead ofdriver(B-03, D288). - postgres.js caps reconnect backoff under 1 s with jitter, defaults
timeouts.acquireto 10 s (OKM1846), swallowsnextWriteon 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 sookm migrate check/applyrefusals are not reported as success on Bun. - node-postgres defaults
timeouts.acquireto 10 s and keeps an idle/checked-out clienterrorlistener 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
timeoutorsignalremoves its checkouterrorlistener, 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 #Nonto the pull request beforelint / 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 testdrops leftoverokm_*tables after each wire and error case, refuses Bun below1.4.0, and never reports an unrun case aspass(B-05, B-06, B-42, D295).okmodel/testingfactories andisolation()work undercompositeTenancy(); concurrent creates no longer false-positive as a factory cycle; matviews are refreshed inisolation()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 stormshort/leaks/readOnly/nestedasserts. okm pushplans from the live catalog and applies each change under a freshpush-<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 pushandokm migrate applyre-stampokm_metainstead of leavingconnect()on OKM1520 (B-21, D289). okm migrate checkkeeps a trigger's function in the logical namespace, so schemas withtimestamps({ enforce: "trigger" })or hand-written triggers no longer fail OKM1020 (B-24, D289).okm checkandokm pushreprint 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
rolesblock applies as a superuser and as aCREATEROLEschema owner: managed roles are created beforeSET 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 afterCREATE ROLE(which leaves none); on Postgres 16+ it grantsWITH SET TRUE(B-30, D293). - Migrate strips the
pgbouncerquery 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 NULLvalidate is no longer OKM1531 (B-37, D289). - An unknown CLI flag is an error;
okm <command> --helplists that command's flags (B-26, D289). - Backfill progress prints a 1-based step index (B-27, D289).
okm pullandokm import drizzlewrite 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. Missingoxfmtprints a notice and still writes the report.okm ext scaffoldincludes 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 pushdocs say it applies the live schema diff aspush-<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, untypedrawand raw projection (B-07, B-08), minimum Bun,okmodel.config.ts,listenas a driver capability only, catalog export vsdescribe, backfill step numbering, and a Not tested section (DM-1 to DM-12, D296).
v0.9.0
✨ Added
dialects
okmodel/pg/unaccent,hstore,ltree,btree_gist,btree_gin,fuzzystrmatch, andpgcryptoare generated extension packs. A feature the declared version does not include fails with OKM1812 and namesminPostgres.vectoris not in this version (D281).
tooling
describe(schema)andbuildCatalog(description)onokmodel/migratereturn the catalog as JSON. The logical identity stayspublicwhen 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 owndrizzle-ormand the same emitter asokm pull. It needs no database. An existing file is OKM1855. A missingdrizzle-ormis OKM1856. Composite foreign keys,relations(), and custom types are listed as not imported (D280).- The GitHub Action
.github/actions/migrateposts one sticky pull-request comment fromokm 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 pullreads 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 undernot pulled yet(D275).okm migrate checkwarns OKM1552 when a domain check or aLANGUAGE sqlfunction body reprints, and OKM1538 warns only when the type change rewrites the heap (D276).external(table)marks a table another catalog owns.okm catalog exportwrites a snapshot whosecatalogHashis 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 pullwrites functions, triggers, views, and materialized views. A view is SQL with its columns. Views andBEGIN ATOMICbodies takedependsOnfrompg_depend. A plpgsql or SQL-string body gets lexicaldependsOncandidates. A scratch replay withcheck_function_bodieson marks each function verified or candidate-only (D278).okm ext scaffold <name>prints a startingextension()definition from the connected catalog.--outwrites 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().ginuses theint4_opsoperator class thatbtree_gincreates.- A
BEGIN ATOMICreprint 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.orderByon 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()readsokm_metaandokm_historyfrom the namespacedefineConfigplaced, so a second catalog's hash is checked at startup.publickeeps 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,orderByon a function call, and computed fields stay deferred (D286).
v0.8.0
✨ Added
dialects
fragmentcomposes SQL inwhereand inselectonfindorone. A value is a parameter.identnames the field, the same namewhereuses, and a hidden field is refused. The tenant predicate stays (D266).rawandexplainrun typed SQL from a table handle. The row class names the columns."read"or"write"is required. A scoped tenant read keeps the tenant predicate outside the fragment. A raw write on that table is OKM1702 and sends nothing.explainreturns the plan outside production and sends nothing whenNODE_ENVisproduction(D268).- The value
fn()returns is callable.slugify(ident("title"))is awhereoperand..as("slug")names aselectentry: a numeric return is a number, and any other return is text.orderBydoes not accept the call.view(name, (q) => q.from(table))selects that table. A column list projects those fields.materializedView()stays SQL plus declared columns (D271).
adapters
okmodel/pg/neonconnects with@neondatabase/serverless.websocketis a pool andtx()works on a direct connection.httpsends one request per statement, andbatch()is the atomic call.tx()on HTTP is OKM1111. A positive timeout is refused. A transaction-mode pooler does not promise session state. With one small compute it reused a backend, so a customSETwas still visible on a second connection (D269).
runtime
insert({ id }, { allow: ["id"] })writes a supplied id on a client-generator primary key. An omitted id in the same list is still generated. Withoutallow, a supplied id is OKM1190. A database default stays OKM1190 whenallownames it. On a list, the type opensidonly whenallowis exactly["id"](D272).okmodel/otelturnshookm,onRoute, andinspect()into OpenTelemetrydb.*spans. You pass the tracer. Statement text stays off unlessstatement: true. Parameters and a tenant id are not recorded. An error span records the error name and an OKM code, and leaves out the message and the stack (D267).- On a topology client,
inspect().routing.reasonmatchesonRoute. The preview does not callonRouteand does not move the selector. A string or a pool stays synchronoussingle-endpoint(D268). connectfromokmodel/pg/neonreturns a promise for a string, a pool, and a topology. Writes go to the primary. A replica can lag, androute: "replica"stays on that replica (D269).
tooling
okm driver test <driver>runs the driver, error, and transaction suites and prints each case as pass, fail, or unsupported, with the capability that explains an unsupported case.okm driver test neonruns those suites. COMMIT loss is unsupported (no socket to cut). The Neon connect gate is 42,000 minified and 14,500 gzip (D269).- A Windows job runs the audit checklist from the packed tarball. It is not a required check. A step that needs a TCP Postgres URL is skipped.
- The repository
.gitattributeskeeps migration SQL, catalog JSON, and.okmfiles LF on checkout (D237).
docs
- Offline-first constraints R1–R7 are recorded for the 0.8 train. They do not start a phase. P100 audits PostgreSQL and writes a report (D265).
- The P100 audit records what PostgreSQL already proves for R1–R7, and the gaps later phases still have to decide. No API change.
- The 0.8 type and byte budget is recorded (P101). No gate or ceiling moves. Ali approves it before P102.
- The drivers page covers Neon: WebSocket and HTTP, the pooler, a cold start, and replica lag (D269).
- The P108 study measures four inline-relation shapes. An opt-in entry types
asandhaswithout moving an existing ceiling. The default-path harvest and a loosereferences()store do not fit. No gate moves. - Inline relations do not ship in 0.8.
relationson the table remains how an include is named (D270). - postgres.js and node-postgres results on Neon primary and primary-pool endpoints are recorded. A cause that was not established stays unclassified (D269).
- The OKE Windows audit records 32 surfaces: 27 work, 2 probably work, none fail, and 3 are unknown (#219).
♻️ Changed
- The spec status names the 0.8.0 release. Decisions run through D274.
🐛 Fixed
okmodel/pg/buntypechecks withoutbun-types. The adapter declarations name the Bun.sql slice they use, so a consumer does not resolve thebunmodule.okm migrate checkandokm checkreprint aBEGIN ATOMICbody before they compare it. An argument qualifier Postgres prints, such asby_id.n, is not OKM1547 (D273).- A raw read renumbers every
$nmarker in a multi-parameter tenant predicate once. Nine user parameters and$1,$2stay$10,$11. - The bundle check leaves
@neondatabase/serverlessexternal, the same way it leavespostgres,pg, and PGlite external (D269). okm checkmatches a table file stem without case, soTasks.tswithimport { tasks } from "./tasks"is not OKM1024.- Packed
okm driver testfindsdist/tooling/driver-run.jswhen the command itself is in a shared chunk. That entry does not import thebunpackage, so Node can runpglite.okm driver test bunloads the Bun adapter from a chunk of the same build and passes the same pool config as the other drivers. - The packed bun driver test builds its own runner, so a Postgres consistency shard does not need a prior
dist/build.
v0.7.0
✨ Added
dialects
minPostgres: 17sets the oldest Postgres major this schema runs on. The majors are 15, 16, 17, and 18. Omitted means 15.requires: { postgres: ">=17" }stores the same floor. The floor is not part of the catalog hash (D261).
runtime
bucket(column, unit, { tz, as })groups an aggregate by a calendar or clock unit. A day or longer returnsTemporal.PlainDate. On atimestamptz, a second, minute, or hour returnsTemporal.Instant. Those three units are refused on adate. A NULL timestamp is one group: the result key is present and the value is null.__proto__,constructor,toString, andhasOwnPropertyare refused asas, and so is anasthat matches another bucket. There is still nohaving. Time-zone date helpers stay deferred (D247, D257).updatewithlock: "skip"andlimitclaims up to that many unlocked rows in one statement. A read still useslock: "update"andwait: "skip"insidetx()(D249).find({ search })andcount({ search })match a column built for search. A string is enough when the table has one. Rows come back closest first unless you passorderBy.page()does not takesearch. Alanguagethat is not text is refused. An empty or malformed language name is OKM1122. A well-formed name is sent to the server (D250).relevance(query, { match, language })orders a search column by the match.matchestakes the same options.{ mode, config }is not accepted (D250).- A to-many
includewithorderByandlimitreturns the first N related rows of each parent. The primary key breaks ties (D251). rowNumber,rank,denseRank, andrunningTotalgo inselectonfindorone. A running total is the column's type, or null.page()refuses a window when the call runs (D251).descendants(id)andancestors(id)read the rows below or above a row in one statement. Passrelationwhen the self-relations use different columns. The fix lists the names. A row already on the path is not entered again.ltreeis not required (D252).sync(id, relation, ids)sets a to-many relation to exactly those ids and reports the links it added and removed (D252).versioned()addsversion. An update passes the version you read and the write bumps it. A stale write is a conflict and changes nothing. Leavingversionout is refused, and so isversionon a table that is not versioned (D254).sortable(groupBy?)addspositionandmove(id, { before })or{ after }. New rows go at the end of the group. A move is O(n) per group and rewrites every row in the group. Naming the group column insetis refused.findandpageorder bypositionwhen you omitorderBy(D254).morph("commentable", ["tasks", "lists"])includes the matching target, with the type set to that table's name, or null.morphManyis the reverse many.morphTosets the type and the id together. Setting one without the other is OKM1121. A type outside the list is OKM1120 (D255).
docs
- The handbook documents those calls, including what each one refuses. Define is Schema, Connect, and Config. Start is Let's start and Basic Usage. Table is a group: one page per part of
table(), from the key through the queries. Keys, Checks, Presets, and Reference rows are split into subsections. - Traits shows
trait(name, { fields }).touchseals those columns and sets them tonow()on update.methodsandrequiresstay OKM1061. Each built-in trait shows the call: insert for timestamps,archiveandrestorefor archivable,versionon update, andmove. - Defaults shows what insert does with each form. A literal and
.defaultSql()are stored and applied when the field is omitted. A generator runs in the application and stays out of the catalog.t.id()andt.identity()are omitted from input..default(null)follows.nullable(). A generated column isstored. - Shape shows the value you pass. An array element cannot be null. A picklist CHECK is
column IN (...), and{ check: false }still rejects a value at encode. Adding an enum label in order plansALTER TYPE … ADD VALUE. Removing or reordering a label replaces the type. - Names: insert and
whereuse the field name.sqlNameis what the statement quotes, and casing does not rewrite a name you set.renamedFromplansRENAME. A comment is checked and left off the catalog. - Visibility splits the options.
.hidden()leaves the column off the row.filterable: trueopenswhereandorderByat runtime..sensitive()takes no argument and redacts logs, errors, andinspect()as[redacted]..guarded()stays closed until{ allow }. A sealed field and a tenant key stay closed. A tenant key update is OKM1704. - Validation splits the options, and each option has a sample.
v.trim()andv.lowercase()take no argument and change the stored string.v.email,v.min, andv.ruletake an issue key. A bareonInsert()oronUpdate()limits the rules that follow. Passing one rule limits that rule and leaves the rest of the list alone.validation: truekeeps the schema'sonReadandstyle.falseturns the table off.insert.validatereturns a frozen row.insert.checkreturns the issues.pickandomitchoose the fields.{ validate: false }skips that call. - References splits each action. The constraint is
{table}_{column}_fkey. A new table adds it withalter table. A table that already exists adds itnot valid, then validates it.onDeleteandonUpdatetakecascade,no action,restrict,set default, orset null, and both can be set together.set nullneeds a nullable column. On a tenant table,set nullandset defaultare refused. Omittedcolumnsuses a one-column primary key. One name selects another column, which has to be the primary key or a unique. - Relations names the include.
oneandmanytakefieldwhen more than one foreign key matches. A to-many include needslimit.has,none, andeveryfilter a relation.manyThroughtakesthrough,from, andto.morphuses${name}Typeand${name}Id. Include istrue.morphTosets both columns. - Indexes take column handles in order. One column, a column order, and several indexes are separate calls. The name is
{table}_{columns}_idxand follows a column or tablesqlNameand snake casing. A new table usescreate index. A table that already exists usescreate index concurrently, outside a transaction..unique()can sit beside a plain index and does not create a unique constraint. - Checks take a name key and
sqltext. The constraint is{table}_{key}_check. The key is not recased. A handle is inserted as its SQL name. A number is inserted as text. Quotes around a string stay in the template. A new table usesalter table. A table that already exists adds the checknot valid, then validates it. A failed row isCheck violationwith kindcheck. filters().parsereturns{ where, orderBy }forfind. Each operator is its own section. A scalar is equality. Several operators becomeand.inandinListbecomeinList.sortis ascending. A relation field becomeshas, and a column of the same name wins. A simple request is one field and a sort. A complex request combines operators, a relation, andsort, then spreads intofind. A hidden field is OKM1123 whenfilters()is called.trait()takesfields,touch,sealed, andpresets. A touched field is sealed. Schema traits apply first.omitDefaultsdrops them and needs a reason. A repeated column is OKM1012. A simple trait adds one field. A complex trait adds a touched column, a sealed column, and a preset.- Tenancy adds a guarded uuid.
for()sets it. Input cannot. A unique includes the key unless.unique({ global }).global("reason")leaves the column off.via()follows up to three to-one relations and does not store the key. A global table cannot reference a tenant table. - A preset is a
whereyou call on the table. A field, an argument, a list, a bound,or, and twowherecalls are separate sections. A chain keeps the caller'swherefirst. Update and delete still need their ownwhere. Insert does not use the preset. A reserved name is OKM1040. A trait preset sees the trait's columns. - Required rows are the
referenceoption. They are rows the application expects, such as statuses. Apply inserts a missing key and does not update or delete. They are not a foreign key and not a migration insert. - Queries add
page, a groupedaggregate, a stringsearch, both tree walks, a listupdate, andinc. A windowasthat clashes with a column is OKM1120.page()still readsselectas field names. - Each table option is its own subsection with a sample.
t.id()showsuuidv7,uuidv4, a client generator, andnone. A tenant unique leaves the key out with{ global: "reason" }.has,none, andeveryeach filter a relation.onConflictis"ignore",{ on, update }, or{ on, return: true }. - Schema is a group under Define. Each option is a page: Tables, Casing, Codecs, Row types, Id default, Minimum Postgres, Traits, Tenancy, Validation, Extensions, Functions, Triggers, and Views.
/docs/define/schemais the overview./docs/define/schema/requiresredirects to Minimum Postgres. - Tables is the list
schema()compiles. A reference names a table in that list, earlier or later. The same name twice, two names that emit one type, or one SQL name twice is OKM1023. A missing name is OKM1020. Swapping the list does not change the catalog hash. - Casing
"snake"rewrites a camelCase table or column.publishedAtispublished_at.userIDisuser_i_d.sqlNameandrenamedFromstay as written. Any other value isschema() casing … must be snake. - Codecs choose the TypeScript value of
t.bigint(),t.identity(), andt.numeric(). The SQL type stays."string"is a decimal string."number"is a s...
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...
v0.5.1
♻️ Changed
- A pull request no longer runs the Postgres suite. The label
needs: postgresruns 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 verifyruns 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 readsCI / area / piece. Long test groups are split (migrateandmigrate pg,driversandrows,schema,columns, andschema pg,tenancy,client, andclient pg,package,contracts, andsafety). Node and Deno areruntimes / portable. Path labels and the pull request lint are jobs in that run. The required status isgate / 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=truefails instead of running. Each skipped Postgres test counts toward a finalN Postgres tests SKIPPEDline (QA-R2, D216). - CI runs the date/time codecs on Node 22 and 24 with
temporal-polyfilland on Node 26 natively, in atemporal / nodejob that joins thegate / checkneeds. 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-costmeasures 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, andrestorenow refuse a where with no effective predicate, including undefined values (QA-C1, QA-H2). A field value is a leaf.eqkeeps 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, andtxwith the function before the options, are now refused (QA-M2). - A bad option on
find,one, andcountnow rejects the query, sosafeandcatchsee OKM1120 (QA-M11).pageandaggregatealready 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.
onRoutereportsfallback: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.
connectedrejects with anunavailableerror 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_tableorundefined_column, with a fix that namesokm migrate applyandokm 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
unavailablewith reasontlsand a fix that namessslmode(QA-L9, D214). filters().parseno longer throws a TypeError onconstructor,__proto__,toString, orhasOwnProperty. A key that is not allowlisted is OKM1123. Parse applieseq,in,notIn,lt,lte,gt,gte,between,startsWith,endsWith,contains,like, andilike. An operator the allow type accepts and parse cannot apply is OKM1120 whenparse()runs (QA-M9, D215).- The shipped
Temporaldeclaration is empty interfaces, so it merges with TypeScript's Temporal lib and withtemporal-polyfill(QA-L2, D215). - Emitted row types keep
jsonb<T>and write an optional insert column asx?: 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
COMMITbefore it drops the client. A fixed delay could reset the socket first, so the transaction rolled back while the caller still gotoutcome_unknown. - A hand-written migration step is linted per statement. A leading comment, a second statement, a semicolon inside a comment,
DOandCALL, andALTER COLUMN … TYPEcan now be refused (QA-H3).-- okm-allowon 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-pooleris set (QA-M6, D215). okm checkon a schema withcitextand 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--verboseorOKM_DEBUG=1(QA-M10, D215). - The published
package.jsonno longer carries theworkspacesfield. 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 Ncounts 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, andokm -hprint the commands and exit 0.okm <command> --helpprints that command (QA-L7, D215).- Apply refuses a database whose
standard_conforming_stringsis off (QA-S2, D215).
docs
- Temporal values keep microseconds. Showing one passes
fractionalSecondDigits. The codecs do not warn (QA-L1, D215). db.viewskeys are camelCased. A materialized view is refreshed withREFRESH MATERIALIZED VIEWuntil arefresh()handle in 0.6 (QA-L10, D215).lists_touchis the triggertimestamps({ enforce: "trigger" })installs on a table namedlists.archivable()installs no trigger (QA-L13, D215).- Hidden columns in
whereandorderByare unchanged. The options for 0.6 are recorded in D215 (QA-S1). timeoutandsignalon Bun.sql and PGlite do not settle a call whose statement is already running, and an outer-client call inside a PGlitetx()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
Temporalglobal. Bun and Node 26 have it; Node 22 and 24 need the polyfill line before the columns are used (QA-D1, D216). - The
okmbin's shebang is node:bunx okmruns with Node,bun --bun okmselects the Bun runtime, and a.tsconfig 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
DOandCALLrule (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().parseoperators (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 VIEWrefresh (QA-D8, D216). - Catalog identity and constraint names wait for 0.6, with the rename design (QA-L4).
v0.5.0
✨ 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 nocloseand nousing.onRoutereports the choice, and a throw from it is ignored.routing.fallbackis"primary"or"error". A required replica that is not eligible is OKM1843. An automatic read withfallback: "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 eitherroutefrom its one endpoint and has nousing.inspect()stayssingle-endpoint. Routing reasons ininspect()are the M2 dev inspector (D202). routing.selectchooses 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.consistencyis"session"(the default) or"eventual". A committed write readspg_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(), andreserve()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"andusing("replica")are OKM1843 while the position is unknown or no replica satisfies it. Healthy replicas that are behind reportfallback:behind(D204).routing.maxLagaccepts"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 customselectreceives that lag in bytes, ornullwhen it is unknown (D204).
tooling
- A migrate target that carries
primary,replicas,weight, orpoolis OKM1845. The command does not resolve that URL. - Issues and pull requests follow one GitHub standard: one
type:label, at least onearea:label, atype(scope):title, and a milestone. A check on the pull request adds thearea:labels from changed paths, then enforces it. Dependabot opens grouped weeklychore(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 thepostgresandtarballjobs: use cases with query counts andisolation(), two preview databases at once from the snapshot, a rehearsal on a populatedTEMPLATEclone, and read-your-write as the application role.docs/example-app.mddescribes 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.selectinstead 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 checkpasses 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, soEXECUTEon a function with named arguments no longer drifts, andokm doctorand 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 --provisionwithrolesno longer grants on the target's own objects (D208).
runtime
db.viewsis typed fromschema({ views })on connected, scoped, and routed clients. Each field is the camel-cased column name, typedstring | 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 checkreads 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 checkwithroleskeeps default privileges in the scratch schema, so it passes and no longer writes them to the target'spublic(D208).- Roles, grants, and default privileges are left out of the stored catalog hash and the
.okmartifact.connect()after an apply withrolesno longer fails with OKM1520 whencatalogDiris not passed.okm checkstill verifies them. A catalog without roles hashes as before (D208). okm migrate applymoves 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 printsrestamped <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 andokm checkis OKM1510 (OKM1529 creates a unique index). Runokm generate archive_predicate, add-- okm-allow OKM1529: rebuilds the same indexabove thecreate unique index concurrentlyline, thenokm migrate apply.okm migrate checkstill 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 untilokm migrate applyruns once (--allow-protectedon a protected target). It printsrestamped <migration id>(D208). okm migrate applyon several databases of one cluster at once no longer fails when they create the same managed role. Every apply but the first failed onpg_authid_rolname_indexwith a raw driver error.CREATE ROLEnow ignores a role another session created first, in the step runner and in the snapshot install, and laterALTER ROLEsteps 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 inokm check(D207).