Skip to content

Releases: omqkhafi/okmodel

v0.10.2

Choose a tag to compare

@github-actions github-actions released this 02 Nov 14:51
e5bc8df

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 only Field <name> was rejected. (OKM1210) in the message, toHttp(), log(), and the cause, as writes already did. This covers a where on find, page, one, count, and exists, values inside inList, relation filters (has, none, every), and the where of update and delete. An ordinary column still names the rejected value.
  • distinctOn reports the default-limit warning (OKM2011) even after an earlier distinct read. 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 (unless allow names them), trait fields, and the primary key on an update. A primary key you set yourself stays in the list on an insert.
  • descendants() and ancestors() on a tree() table now need an options argument with a limit in TypeScript when connect does not set limits.find, the same as find(). A bare descendants(id) used to compile and then throw OKM1101. With limits.find set, 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 with pg_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 seed now 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 keeps values() undefined and a reduced cause even with errors.includeValues.
  • The seed docs explain that relative imports in a seed file use the .ts extension under bunx okm, with an example that seeds a global tenants table and then tenant rows.

v0.10.1

Choose a tag to compare

@github-actions github-actions released this 01 Nov 19:24
03c7946

💥 Breaking Changes

  • descendants() and ancestors() on a tree() table now need a row bound, the same as find(). Pass limit, set a default with connect({ limits: { find } }), or read every row with .all(reason). Without one the call throws OKM1101 (descendants() needs a limit.). A limit above limits.max is OKM1101 too. depth still 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 idleTimeoutMs on connect or open to change it, or idleTimeoutMs: 0 to keep idle connections open as before. A pool reconnects on the next query, including inside db.with() and tx(). A short script that ends without await db.close() can now wait for the idle timer before it exits; call db.close() or set idleTimeoutMs: 0. Neon over HTTP refuses the option (OKM1120), and PGlite has no pool to time out.

✨ Added

  • When connect({ limits }) sets limits.find or limits.page, find() and page() no longer require limit in TypeScript, so a bare find() compiles and uses that default. Each key works on its own, and max does not change the types. A client without limits, including testing(), still requires limit.
  • okm init writes limits: { 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 --limits adds 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 error 2BP01 when the generated column's name sorts before one of its sources. The catalog hash and existing migration files do not change.
  • onQuery on a db.with() client reports one event per call. The settings prelude, BEGIN, COMMIT, and RESET ALL are no longer reported as separate queries.
  • WriteEvent.settings lists the setting names, never the values, for writes through a db.with() client, including tx(), typed raw(), archive, and restore. It used to be empty.
  • Under for(), warning OKM2005 no longer fires when orderBy ends on the rest of a unique key that starts with the pinned tenant key.
  • distinct and distinctOn reads honor limits.find and limits.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 when process.cwd is missing.

🔥 Removed

  • Error code OKM1025. It was registered but never thrown: two Register declarations that give schema different 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

Choose a tag to compare

@github-actions github-actions released this 28 Oct 09:25
5a442d5

💥 Breaking Changes

  • DriverCapabilities no longer includes prepared or describe. Prepared-statement mode stays a connect option. A driver that can describe a statement still exposes pool.describe.
  • hidden({ filterable }) accepts only a literal true. false and a boolean variable are type errors. Call hidden() with no argument. Passing false from JavaScript still leaves the column hidden and not filterable.
  • okm migrate plan --json finding reason is an untrusted object. okm migrate baseline --json unmanaged name is an untrusted object. okm migrate status JSON id, kind, and state are untrusted objects. A database error's text is errors[].detail. summary stays a registry sentence, or A database error was returned (<kind>). when the code has no page.
  • db.batch of a versioned update throws OKM1121 before any statement; move that update into tx(). tx.batch is unchanged, and version: "any" stays allowed.
  • Bound parameters stay off an error cause unless includeValues is set. A page cursor for a hidden or sensitive orderBy carries only the primary key (a gone row is OKM1130), and OkmError is branded so instanceof matches 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: false turns 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 next okm generate writes nothing. An index that does not is replaced once: CREATE INDEX CONCURRENTLY for the new index, then DROP INDEX CONCURRENTLY for the old one. That is not a blocking rebuild, and okm migrate check accepts it (Q3, D306, #390). OKM1706 stays when prefixing is off.
  • With validation: true, an unknown key on insert, update, upsert / onConflict, first-or-create, insert.validate, insert.check, insert.pick(...).validate, insert.pick(...).check, insert.omit(...).validate, insert.omit(...).check, update.validate, or insert["~standard"].validate is OKM1120 and names the key and the table (R1, D304). A typo-only update names the key instead of saying "update set is empty." insert.check, pick(...).check, and omit(...).check return 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 Schema validate throws 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 with version in set is 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 of version stays OKM1190.
  • okm generate exits 1 when an error finding exists; the migration file is still written; stderr says okm migrate apply will refuse until it is fixed (Q8, #395). A warning-only finding stays exit 0.
  • okm pull and okm import drizzle write relative imports with the .ts extension so Node type stripping and bunx okm can load the schema (R4, #401). tsc needs allowImportingTsExtensions with noEmit, or rewriteRelativeImportExtensions.
  • An update on a versioned() table without version is a TypeScript error; the runtime OKM1120 stays when types are bypassed (Q5, D305, #392). Non-versioned tables are unchanged.
  • okm exit codes follow the outcome. 0 is done or nothing to do. 1 is a command that ran and failed. 2 is usage or configuration (unknown flag, missing config, a Node older than 22.18). 3 is a decision the command will not ask (OKM1530, and a committed step that needs a person: OKM1553, OKM1555). 4 is a guard (OKM1850, OKM1852, OKM1855, OKM1859). 5 is 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, stays 1. A script that only checked non-zero still fails. A script that treated every failure as 1 now sees 2 through 5.
  • okm migrate plan --json, okm migrate baseline --json, and okm catalog export --json keep their previous fields inside the okm.cli/1 envelope data. parsePlanJson reads data and still accepts a bare plan document.
  • okm build and okm dev reject 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 next okm generate plans 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 because sha256.ts leaves it (90,763 to 88,348).
  • schema() resolves every cased name once: snake splits an acronym (userID is user_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. tenantIndexes on a global table, or a tenant-index mark when the schema has no tenancy, is OKM1060, and OKM1706 on a using index names index().global("reason") (#546).
  • A table named capabilities is OKM1023, because db.capabilities is reserved on the root client, a scoped client, and inside tx() (#546).
  • find, one, and page reject a plain hidden key, an unknown key, a wrong filterable value, a bad or() branch, and a hidden orderBy, including inside include, and page rejects an unknown option and relevance() (OKM1120). The same plain hidden key and a bad or() are type errors on count, exists, update, and delete, while a filterable hidden column stays valid in where and orderBy (#549, #550).

✨ Added

contracts

  • DescribeResult adds parameters (type OIDs in $n order) and fields (name, typeOid, tableOid, attnum). columns and parameterCount stay.
  • ExecuteResult.reported is the driver command-tag count, or null when the driver reported none. Absent on fixtures that only set count. The caller-facing count stays reported ?? rows.length.

adapters

  • postgres.js and PGlite describe return those fields and do not execute the statement. node-postgres, Bun.sql, and Neon still leave describe unset.
  • resultFrom records the command tag on ExecuteResult.reported. postgres.js and Bun.sql use count. pg and Neon use rowCount. PGlite uses per-statement rowCount, and cumulative affectedRows only when rowCount is 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 a versioned() table skips the version check, bumps version by 1, and returns the claimed rows. A miss is an empty array. version stays required on every other versioned update. version: "any" without lock: "skip" and limit, an unrecognised version string, and a version key in set are 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 with hookm.onTransaction. Normative shape in docs/observer-events.md. db.flush() waits for the okmodel/observe helper (app size gate).
  • okmodel/observe exports writeObserver(), flush(), OBSERVE_BUFFER_MAX (10,000), and observerBufferOverflow() (OKM2200). The helper buffers per unit, calls exactly one of committed | rolledBack | unknown, isolates throwing and rejecting handlers via onObserverError, and settles tracked promises on flush(). Overflow drops the oldest unit without blocking the write path.
  • writeObserver({ keys }) names schema tables. Those writes append primary-key RETURNING when the statement is a simple insert, update, or delete, and put the values on WriteEvent.keys. sql() and inspect() 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. Nested RETURNING (parent guard, versioned close, archive) is not rewritten. A caller result is unchanged: observer-only rows are hidden.
  • Write events take rowCount from the command tag. Mode is changed for insert, update, and delete, returned when RETURNING ran, and unknown with rowCount: null when the driver gave no tag. An upsert stays changed and 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/warnings exports warnings(). Pass it to connect({ 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) ...
Read more

v0.9.1

Choose a tag to compare

@github-actions github-actions released this 17 Oct 17:51

💥 Breaking Changes

  • Breaking-ish: now refused — a backslash in fragment / raw template 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> --help lists that command's flags (B-26, D289).
  • Breaking-ish: now refused — an update that would set a tree parent to the row itself or to a descendant is OKM1120 when where is 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, and internalQuery stay off). Tables without sensitive columns keep today's cause. includeValues remains the explicit opt-in (B-33, D291).
  • With defineConfig({ roles }), managed functions revoke EXECUTE from PUBLIC and 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 reuse tasks.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 and inList (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 update that would set a tree parent to the row itself or to a descendant is OKM1120 when where is the primary key (B-10, D295).
  • A throw from a custom routing.select keeps an OkmError's kind; a plain throw becomes kind invalid instead of driver (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 COMMIT completed destroys the connection and does not fail the call on Bun.sql, node-postgres, or Neon, so a committed write is reported as committed and afterCommit still runs (B-04, D288).
  • node-postgres maps Client has encountered a connection error and is not queryable to 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 SQLSTATE 08006, so a checkout that fails before the reserved-handle guard classifies as unavailable instead of driver (B-03, D288).
  • postgres.js caps reconnect backoff under 1 s with jitter, defaults timeouts.acquire to 10 s (OKM1846), swallows nextWrite on 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 so okm migrate check / apply refusals are not reported as success on Bun.
  • node-postgres defaults timeouts.acquire to 10 s and keeps an idle/checked-out client error listener 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 timeout or signal removes its checkout error listener, 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 #N onto the pull request before lint / 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 test drops leftover okm_* tables after each wire and error case, refuses Bun below 1.4.0, and never reports an unrun case as pass (B-05, B-06, B-42, D295).
  • okmodel/testing factories and isolation() work under compositeTenancy(); concurrent creates no longer false-positive as a factory cycle; matviews are refreshed in isolation() 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 storm short/leaks/readOnly/nested asserts.
  • okm push plans from the live catalog and applies each change under a fresh push-<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 push and okm migrate apply re-stamp okm_meta instead of leaving connect() on OKM1520 (B-21, D289).
  • okm migrate check keeps a trigger's function in the logical namespace, so schemas with timestamps({ enforce: "trigger" }) or hand-written triggers no longer fail OKM1020 (B-24, D289).
  • okm check and okm push reprint 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 roles block applies as a superuser and as a CREATEROLE schema owner: managed roles are created before SET 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 after CREATE ROLE (which leaves none); on Postgres 16+ it grants WITH SET TRUE (B-30, D293).
  • Migrate strips the pgbouncer query 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 NULL validate is no longer OKM1531 (B-37, D289).
  • An unknown CLI flag is an error; okm <command> --help lists that command's flags (B-26, D289).
  • Backfill progress prints a 1-based step index (B-27, D289).
  • okm pull and okm import drizzle write 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. Missing oxfmt prints a notice and still writes the report. okm ext scaffold includes 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 push docs say it applies the live schema diff as push-<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, untyped raw and raw projection (B-07, B-08), minimum Bun, okmodel.config.ts, listen as a driver capability only, catalog export vs describe, backfill step numbering, and a Not tested section (DM-1 to DM-12, D296).

v0.9.0

Choose a tag to compare

@github-actions github-actions released this 17 Oct 00:06
a59e009

✨ Added

dialects

  • okmodel/pg/unaccent, hstore, ltree, btree_gist, btree_gin, fuzzystrmatch, and pgcrypto are generated extension packs. A feature the declared version does not include fails with OKM1812 and names minPostgres. vector is not in this version (D281).

tooling

  • describe(schema) and buildCatalog(description) on okmodel/migrate return the catalog as JSON. The logical identity stays public when 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 own drizzle-orm and the same emitter as okm pull. It needs no database. An existing file is OKM1855. A missing drizzle-orm is OKM1856. Composite foreign keys, relations(), and custom types are listed as not imported (D280).
  • The GitHub Action .github/actions/migrate posts one sticky pull-request comment from okm 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 pull reads 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 under not pulled yet (D275).
  • okm migrate check warns OKM1552 when a domain check or a LANGUAGE sql function body reprints, and OKM1538 warns only when the type change rewrites the heap (D276).
  • external(table) marks a table another catalog owns. okm catalog export writes a snapshot whose catalogHash is 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 pull writes functions, triggers, views, and materialized views. A view is SQL with its columns. Views and BEGIN ATOMIC bodies take dependsOn from pg_depend. A plpgsql or SQL-string body gets lexical dependsOn candidates. A scratch replay with check_function_bodies on marks each function verified or candidate-only (D278).
  • okm ext scaffold <name> prints a starting extension() definition from the connected catalog. --out writes 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().gin uses the int4_ops operator class that btree_gin creates.
  • A BEGIN ATOMIC reprint 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. orderBy on 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() reads okm_meta and okm_history from the namespace defineConfig placed, so a second catalog's hash is checked at startup. public keeps 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, orderBy on a function call, and computed fields stay deferred (D286).

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 16 Oct 16:52
d1a6cbf

✨ Added

dialects

  • fragment composes SQL in where and in select on find or one. A value is a parameter. ident names the field, the same name where uses, and a hidden field is refused. The tenant predicate stays (D266).
  • raw and explain run 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. explain returns the plan outside production and sends nothing when NODE_ENV is production (D268).
  • The value fn() returns is callable. slugify(ident("title")) is a where operand. .as("slug") names a select entry: a numeric return is a number, and any other return is text. orderBy does 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/neon connects with @neondatabase/serverless. websocket is a pool and tx() works on a direct connection. http sends one request per statement, and batch() 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 custom SET was 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. Without allow, a supplied id is OKM1190. A database default stays OKM1190 when allow names it. On a list, the type opens id only when allow is exactly ["id"] (D272).
  • okmodel/otel turns hookm, onRoute, and inspect() into OpenTelemetry db.* spans. You pass the tracer. Statement text stays off unless statement: 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.reason matches onRoute. The preview does not call onRoute and does not move the selector. A string or a pool stays synchronous single-endpoint (D268).
  • connect from okmodel/pg/neon returns a promise for a string, a pool, and a topology. Writes go to the primary. A replica can lag, and route: "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 neon runs 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 .gitattributes keeps migration SQL, catalog JSON, and .okm files 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 as and has without moving an existing ceiling. The default-path harvest and a loose references() store do not fit. No gate moves.
  • Inline relations do not ship in 0.8. relations on 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/bun typechecks without bun-types. The adapter declarations name the Bun.sql slice they use, so a consumer does not resolve the bun module.
  • okm migrate check and okm check reprint a BEGIN ATOMIC body before they compare it. An argument qualifier Postgres prints, such as by_id.n, is not OKM1547 (D273).
  • A raw read renumbers every $n marker in a multi-parameter tenant predicate once. Nine user parameters and $1, $2 stay $10, $11.
  • The bundle check leaves @neondatabase/serverless external, the same way it leaves postgres, pg, and PGlite external (D269).
  • okm check matches a table file stem without case, so Tasks.ts with import { tasks } from "./tasks" is not OKM1024.
  • Packed okm driver test finds dist/tooling/driver-run.js when the command itself is in a shared chunk. That entry does not import the bun package, so Node can run pglite. okm driver test bun loads 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

Choose a tag to compare

@github-actions github-actions released this 15 Oct 17:47
178bbca

✨ Added

dialects

  • minPostgres: 17 sets 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 returns Temporal.PlainDate. On a timestamptz, a second, minute, or hour returns Temporal.Instant. Those three units are refused on a date. A NULL timestamp is one group: the result key is present and the value is null. __proto__, constructor, toString, and hasOwnProperty are refused as as, and so is an as that matches another bucket. There is still no having. Time-zone date helpers stay deferred (D247, D257).
  • update with lock: "skip" and limit claims up to that many unlocked rows in one statement. A read still uses lock: "update" and wait: "skip" inside tx() (D249).
  • find({ search }) and count({ search }) match a column built for search. A string is enough when the table has one. Rows come back closest first unless you pass orderBy. page() does not take search. A language that 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. matches takes the same options. { mode, config } is not accepted (D250).
  • A to-many include with orderBy and limit returns the first N related rows of each parent. The primary key breaks ties (D251).
  • rowNumber, rank, denseRank, and runningTotal go in select on find or one. A running total is the column's type, or null. page() refuses a window when the call runs (D251).
  • descendants(id) and ancestors(id) read the rows below or above a row in one statement. Pass relation when the self-relations use different columns. The fix lists the names. A row already on the path is not entered again. ltree is 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() adds version. An update passes the version you read and the write bumps it. A stale write is a conflict and changes nothing. Leaving version out is refused, and so is version on a table that is not versioned (D254).
  • sortable(groupBy?) adds position and move(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 in set is refused. find and page order by position when you omit orderBy (D254).
  • morph("commentable", ["tasks", "lists"]) includes the matching target, with the type set to that table's name, or null. morphMany is the reverse many. morphTo sets 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 }). touch seals those columns and sets them to now() on update. methods and requires stay OKM1061. Each built-in trait shows the call: insert for timestamps, archive and restore for archivable, version on update, and move.
  • 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() and t.identity() are omitted from input. .default(null) follows .nullable(). A generated column is stored.
  • 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 plans ALTER TYPE … ADD VALUE. Removing or reordering a label replaces the type.
  • Names: insert and where use the field name. sqlName is what the statement quotes, and casing does not rewrite a name you set. renamedFrom plans RENAME. A comment is checked and left off the catalog.
  • Visibility splits the options. .hidden() leaves the column off the row. filterable: true opens where and orderBy at runtime. .sensitive() takes no argument and redacts logs, errors, and inspect() 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() and v.lowercase() take no argument and change the stored string. v.email, v.min, and v.rule take an issue key. A bare onInsert() or onUpdate() limits the rules that follow. Passing one rule limits that rule and leaves the rest of the list alone. validation: true keeps the schema's onRead and style. false turns the table off. insert.validate returns a frozen row. insert.check returns the issues. pick and omit choose the fields. { validate: false } skips that call.
  • References splits each action. The constraint is {table}_{column}_fkey. A new table adds it with alter table. A table that already exists adds it not valid, then validates it. onDelete and onUpdate take cascade, no action, restrict, set default, or set null, and both can be set together. set null needs a nullable column. On a tenant table, set null and set default are refused. Omitted columns uses a one-column primary key. One name selects another column, which has to be the primary key or a unique.
  • Relations names the include. one and many take field when more than one foreign key matches. A to-many include needs limit. has, none, and every filter a relation. manyThrough takes through, from, and to. morph uses ${name}Type and ${name}Id. Include is true. morphTo sets both columns.
  • Indexes take column handles in order. One column, a column order, and several indexes are separate calls. The name is {table}_{columns}_idx and follows a column or table sqlName and snake casing. A new table uses create index. A table that already exists uses create 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 sql text. 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 uses alter table. A table that already exists adds the check not valid, then validates it. A failed row is Check violation with kind check.
  • filters().parse returns { where, orderBy } for find. Each operator is its own section. A scalar is equality. Several operators become and. in and inList become inList. sort is ascending. A relation field becomes has, and a column of the same name wins. A simple request is one field and a sort. A complex request combines operators, a relation, and sort, then spreads into find. A hidden field is OKM1123 when filters() is called.
  • trait() takes fields, touch, sealed, and presets. A touched field is sealed. Schema traits apply first. omitDefaults drops 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 where you call on the table. A field, an argument, a list, a bound, or, and two where calls are separate sections. A chain keeps the caller's where first. Update and delete still need their own where. Insert does not use the preset. A reserved name is OKM1040. A trait preset sees the trait's columns.
  • Required rows are the reference option. 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 grouped aggregate, a string search, both tree walks, a list update, and inc. A window as that clashes with a column is OKM1120. page() still reads select as field names.
  • Each table option is its own subsection with a sample. t.id() shows uuidv7, uuidv4, a client generator, and none. A tenant unique leaves the key out with { global: "reason" }. has, none, and every each filter a relation. onConflict is "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/schema is the overview. /docs/define/schema/requires redirects 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. publishedAt is published_at. userID is user_i_d. sqlName and renamedFrom stay as written. Any other value is schema() casing … must be snake.
  • Codecs choose the TypeScript value of t.bigint(), t.identity(), and t.numeric(). The SQL type stays. "string" is a decimal string. "number" is a s...
Read more

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 12 Oct 19:23
5deaa1f

💥 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...
Read more

v0.5.1

Choose a tag to compare

@github-actions github-actions released this 08 Oct 20:09
b202786

♻️ Changed

  • A pull request no longer runs the Postgres suite. The label needs: postgres runs the suite on 15 and 18 and the tarball on 18, in the same Actions run as the other checks. Adding that label without a push starts it on its own and does not cancel the run. bun run verify runs that suite on this machine. The release and the weekly run still cover 15 through 18 (D212).
  • A pull request update is one Actions run. Checks are named area / piece, so each one reads CI / area / piece. Long test groups are split (migrate and migrate pg, drivers and rows, schema, columns, and schema pg, tenancy, client, and client pg, package, contracts, and safety). Node and Deno are runtimes / portable. Path labels and the pull request lint are jobs in that run. The required status is gate / check (D213).
  • Running the Postgres suite with no database is loud. The preflight names the unreachable primary and how many files would skip, and CI=true fails instead of running. Each skipped Postgres test counts toward a final N Postgres tests SKIPPED line (QA-R2, D216).
  • CI runs the date/time codecs on Node 22 and 24 with temporal-polyfill and on Node 26 natively, in a temporal / node job that joins the gate / check needs. The Postgres suite workflow proves the D215 pid probe refuses a transaction-mode pgbouncer with OKM1854 on every suite run (QA-R3, D216).
  • bun run type-cost measures a 250-table inferred schema beside the other probes and fails above measured +20% (21,180 instantiations). Check time is reported, not gated (QA-R4, D216).

🐛 Fixed

runtime

  • update, delete, archive, and restore now refuse a where with no effective predicate, including undefined values (QA-C1, QA-H2). A field value is a leaf. eq keeps a timestamp, a date, bytes, or a jsonb object in the filter. A bare object stays OKM1121 (D125, D210).
  • An or() branch with no effective predicate is now refused, in reads and in writes (QA-H1). or([]) still matches nothing.
  • or() with a non-array or with more than one argument is now refused (QA-M4).
  • An unknown key on update, and tx with the function before the options, are now refused (QA-M2).
  • A bad option on find, one, and count now rejects the query, so safe and catch see OKM1120 (QA-M11). page and aggregate already did.
  • A read on a replica that refuses or drops the connection falls back to the primary on postgres.js, node-postgres, and Bun.sql. onRoute reports fallback:unhealthy. A driver error with an empty message now has text (QA-M1, D214).
  • Closing a client before it connects no longer leaves an unhandled rejection. connected rejects with an unavailable error that says the client was closed first (QA-M5, D214).
  • OKM1104 lists each accepted constraint once and does not name the tenant key, which every unique includes (QA-L11, D214).
  • OKM1200 reads Validation failed: <field> <reason> and names the first failing field (QA-L12, D214).

dialects

  • Compiled SQL for a tenant table keeps the tenant predicate on every table alias, including a where that compares an object through eq (QA-S4).
  • A missing table or column maps to undefined_table or undefined_column, with a fix that names okm migrate apply and okm migrate status. Calls that share one caller signal now share one abort listener instead of adding one each (QA-L8, D214).
  • A TLS or certificate failure maps to unavailable with reason tls and a fix that names sslmode (QA-L9, D214).
  • filters().parse no longer throws a TypeError on constructor, __proto__, toString, or hasOwnProperty. A key that is not allowlisted is OKM1123. Parse applies eq, in, notIn, lt, lte, gt, gte, between, startsWith, endsWith, contains, like, and ilike. An operator the allow type accepts and parse cannot apply is OKM1120 when parse() runs (QA-M9, D215).
  • The shipped Temporal declaration is empty interfaces, so it merges with TypeScript's Temporal lib and with temporal-polyfill (QA-L2, D215).
  • Emitted row types keep jsonb<T> and write an optional insert column as x?: T (QA-L3, D215).
  • fn({ searchPath }) is checked as a comma-separated list of identifiers when the function is built (QA-S3, D215).

tooling

  • The commit-cut harness waits until the server has answered COMMIT before it drops the client. A fixed delay could reset the socket first, so the transaction rolled back while the caller still got outcome_unknown.
  • A hand-written migration step is linted per statement. A leading comment, a second statement, a semicolon inside a comment, DO and CALL, and ALTER COLUMN … TYPE can now be refused (QA-H3). -- okm-allow on the step silences that code on every statement of the step.
  • Port 6432 counts as a pooler. Apply, seed, and check refuse a backend pid that changes between two statements, with OKM1854, unless --allow-pooler is set (QA-M6, D215).
  • okm check on a schema with citext and a view exits 0. The scratch search path includes the schemas that hold extension types, then restores the previous value (QA-M7, D215).
  • isolation() uses a new tenant pair each run, asserts only on the row it inserted, deletes that row, and names a missing factory. It covers aggregate, page, batch, and stream when the client has them (QA-M8, D215).
  • A CLI error prints error <code>: <message> and the fix, and exits 1. The stack is printed only with --verbose or OKM_DEBUG=1 (QA-M10, D215).
  • The published package.json no longer carries the workspaces field. The pack step strips it from the tarball only; the root keeps it, and the tarball smoke test asserts it (QA-R1, D216).
  • Apply's failed at step N counts from 1, the same number the linter prints. Status still prints the stored index (QA-L5, D215).
  • A renamed table's plan row estimate uses the old table's row count (QA-L6, D215).
  • okm, okm help, okm --help, and okm -h print the commands and exit 0. okm <command> --help prints that command (QA-L7, D215).
  • Apply refuses a database whose standard_conforming_strings is off (QA-S2, D215).

docs

  • Temporal values keep microseconds. Showing one passes fractionalSecondDigits. The codecs do not warn (QA-L1, D215).
  • db.views keys are camelCased. A materialized view is refreshed with REFRESH MATERIALIZED VIEW until a refresh() handle in 0.6 (QA-L10, D215).
  • lists_touch is the trigger timestamps({ enforce: "trigger" }) installs on a table named lists. archivable() installs no trigger (QA-L13, D215).
  • Hidden columns in where and orderBy are unchanged. The options for 0.6 are recorded in D215 (QA-S1).
  • timeout and signal on Bun.sql and PGlite do not settle a call whose statement is already running, and an outer-client call inside a PGlite tx() callback queues behind the transaction instead of throwing (QA-M3, D216). Neither driver can cancel an in-flight statement, and the client-side race did not fit the size cap, so both stay known limits. OKM1192 is reserved for the later refusal.
  • Date and time columns need a Temporal global. Bun and Node 26 have it; Node 22 and 24 need the polyfill line before the columns are used (QA-D1, D216).
  • The okm bin's shebang is node: bunx okm runs with Node, bun --bun okm selects the Bun runtime, and a .ts config needs Node 22.18 or newer (QA-D2, D216).
  • The update/delete rule names its codes: no effective predicate, including undefined values, is OKM1102, and an empty or() branch is OKM1121 (QA-D3, D216).
  • The linter page states the statement split, comment handling, and the DO and CALL rule (QA-D4, D216).
  • Circuits start closed, so the first read goes to a replica without waiting for a probe (QA-D5, D216).
  • Known limits records the M3 outcome, sub-microsecond rounding on write, the PGlite outer-client queue rule, and the thirteen filters().parse operators (QA-D6, D216).
  • Production states the pooler refusal: the refused hosts and ports, the pid probe, and --allow-pooler (QA-D7, D216).
  • The views section shows the camelCase handle and the REFRESH MATERIALIZED VIEW refresh (QA-D8, D216).
  • Catalog identity and constraint names wait for 0.6, with the rename design (QA-L4).

v0.5.0

Choose a tag to compare

@github-actions github-actions released this 07 Oct 13:41
fe9fad8

✨ Added

runtime

  • connect({ primary, replicas }) opens one pool per endpoint and probes each replica. A string or a pool is unchanged and does not load that code (D201).
  • Reads on a topology go to an eligible replica. route: "primary" or "replica" forces a read. db.using("primary" | "replica") returns a client with no close and no using. onRoute reports the choice, and a throw from it is ignored. routing.fallback is "primary" or "error". A required replica that is not eligible is OKM1843. An automatic read with fallback: "error" and no eligible replica is OKM1844. A write, transaction, or locking read on a replica scope is OKM1840. A string or pool client serves either route from its one endpoint and has no using. inspect() stays single-endpoint. Routing reasons in inspect() are the M2 dev inspector (D202).
  • routing.select chooses the replica: weighted (the default, smooth weighted round-robin), roundRobin, leastConnections, latencyAware, or a function. A weight of 0 is OKM1120. A saturated replica is skipped on an automatic read. If every healthy replica is saturated, the read uses the primary (fallback:saturated) or OKM1844. route: "replica" still uses a saturated replica. A connection failure retries once with the same strategy (D203).
  • routing.consistency is "session" (the default) or "eventual". A committed write reads pg_current_wal_insert_lsn() before its promise resolves when replicas are configured. That is one extra round trip. Later reads use a replica that has replayed at least that far. The root, for(), unscoped(), using(), and reserve() share that watermark. A failed position read keeps automatic reads on the primary (fallback:position-unknown, or OKM1844) until the next successful read. A primary probe already in flight does not clear that state; only a probe started in the same unknown generation does. route: "replica" and using("replica") are OKM1843 while the position is unknown or no replica satisfies it. Healthy replicas that are behind report fallback:behind (D204).
  • routing.maxLag accepts "5s", "500ms", "2m", "16MB", "512KB", "1GB", and "4096B". A bare number or any other form is OKM1120. A caught-up replica has lag zero. A custom select receives that lag in bytes, or null when it is unknown (D204).

tooling

  • A migrate target that carries primary, replicas, weight, or pool is OKM1845. The command does not resolve that URL.
  • Issues and pull requests follow one GitHub standard: one type: label, at least one area: label, a type(scope): title, and a milestone. A check on the pull request adds the area: labels from changed paths, then enforces it. Dependabot opens grouped weekly chore(deps) updates, labelled, with no issue or milestone required (D206).

docs

  • A reference app in packages/reference-app (private, not published): a multi-tenant project tracker on public entry points, with traits, archive, three generated migrations, a function, views, and the replica topology. CI runs it in the postgres and tarball jobs: use cases with query counts and isolation(), two preview databases at once from the snapshot, a rehearsal on a populated TEMPLATE clone, and read-your-write as the application role. docs/example-app.md describes it (D207).
  • The README covers the 0.5 topology: connect({ primary, replicas }), automatic read routing, route, db.using, routing.select, consistency, onRoute, and OKM1840, OKM1843, OKM1844, OKM1845, and OKM1846, plus the reference app as the worked example. The session-watermark cost and the fallback rate under write load are recorded for a primary and two standbys (D209).

♻️ Changed

  • An automatic read uses routing.select instead of the first healthy replica.
  • The session wrote flag is gone. A read after a committed write follows the commit position.

🐛 Fixed

dialects

  • View scratch verification creates the schema's functions before its views, so okm check passes for a view that calls a declared function (D208).
  • archivable() writes the partial unique index predicate the way Postgres prints it, (archived_at IS NULL), so a unique column on an archivable table no longer drifts after every apply. Stored catalogs change once (D208).
  • A function grant names its argument types from oidvectortypes, so EXECUTE on a function with named arguments no longer drifts, and okm doctor and the apply preflight check that grant instead of skipping it (OKM1825) (D208).
  • Grants and default privileges target the schema they are applied to. okm migrate check --provision with roles no longer grants on the target's own objects (D208).

runtime

  • db.views is typed from schema({ views }) on connected, scoped, and routed clients. Each field is the camel-cased column name, typed string | null. Types only (D208).
  • Smooth weighted round-robin clears the current weight of a replica that did not take part in a pick. After that replica is eligible again, a heavier replica keeps the next read (D205).

tooling

  • okm migrate check reads each scratch schema with that schema as the search path. A declared view and a function that takes a schema enum no longer fail with OKM1547 or OKM1020 (D208).
  • okm migrate check with roles keeps default privileges in the scratch schema, so it passes and no longer writes them to the target's public (D208).
  • Roles, grants, and default privileges are left out of the stored catalog hash and the .okm artifact. connect() after an apply with roles no longer fails with OKM1520 when catalogDir is not passed. okm check still verifies them. A catalog without roles hashes as before (D208).
  • okm migrate apply moves a hash stored before D208 to the new hash when it is exactly the old hash of a migration already applied, also when nothing is pending, and prints restamped <migration id>. A protected target needs --allow-protected. Any other hash is left as drift (D208).
  • Upgrade, archivable with a unique column: after upgrading, connect() is OKM1520 and okm check is OKM1510 (OKM1529 creates a unique index). Run okm generate archive_predicate, add -- okm-allow OKM1529: rebuilds the same index above the create unique index concurrently line, then okm migrate apply. okm migrate check still reports OKM1547 at the migration that first created the index, as it did before; see known limits (D208).
  • Upgrade, a database applied with roles: after upgrading, connect() is OKM1520 until okm migrate apply runs once (--allow-protected on a protected target). It prints restamped <migration id> (D208).
  • okm migrate apply on several databases of one cluster at once no longer fails when they create the same managed role. Every apply but the first failed on pg_authid_rolname_index with a raw driver error. CREATE ROLE now ignores a role another session created first, in the step runner and in the snapshot install, and later ALTER ROLE steps still run (D207).
  • isolation() no longer fails with OKM1120 on a schema with a tenant-scoped view. It checks tables only; a view that hides the tenant key is still OKM1820 in okm check (D207).