0.5

Every published OKModel release, newest first.

all series · 0.6 · 0.4

v0.5.1

♻️ 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

✨ 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).