0.7

Every published OKModel release, newest first.

all series · 0.8 · 0.6

v0.7.0

✨ 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 safe integer for bigint and can lose digits on numeric. "bigint" is a JavaScript bigint. A schema codec other than "string" needs the same as on every column of that type. Timestamps stay "temporal".
  • Row types store "emitted" or "inferred". Omitting the field is "emitted". Queries still infer from schema(). okm build writes .okm/types.d.ts for either value. notes becomes Notes, NotesInsert, and NotesUpdate. A t.jsonb<T>() argument is copied from the schema source. Any other value is schema() types … must be emitted or inferred.
  • Id default replaces a bare t.id(). Omitted and "uuidv4" store gen_random_uuid(). "uuidv7" stores uuidv7() and needs Postgres 18. A client generator is filled on insert and stays out of the catalog. okid() is text with collation C. A column default wins. "none" is that column option. A Postgres floor below 18 on uuidv7() is OKM1812.
  • Minimum Postgres is minPostgres, a major from 15 through 18. Omitted means 15. requires: { postgres: ">=17" } stores the same floor and is deprecated. A bare t.id() stores gen_random_uuid(). An explicit uuidv7() on a floor below 18 is OKM1812. A server below the floor is OKM1802.
  • Traits is a list applied before a table's own traits. An omitted list and [] add nothing. omitDefaults drops the schema list and needs a reason. A repeated column is OKM1012. timestamps({ enforce: "trigger" }) adds okm_touch_<column> and a before-update trigger.
  • Tenancy is columnTenancy(), compositeTenancy(), or rlsTenancy(). Omitted, a table that sets tenancy is OKM1060. Each key is a guarded uuid filled by for(). rlsTenancy keeps the column predicate and refuses the owner, a superuser, and BYPASSRLS (OKM1707).
  • Validation is stored as given. schema() does not run the rules. Omitted and false are off. true and an object are on. style is inline or section. A field in both places is OKM1030. A write without okmodel/validate is OKM1201.
  • Extensions is a list of citext(), pgTrgm(), or extension("name"). The record is contributed when the catalog is read. An omitted citext column is OKM1020. A list that names another extension is OKM1810. The same name twice is OKM1813. An exact version is written. A >= floor is stored and is not written.
  • Functions is a list of fn(). plpgsql needs dependsOn (OKM1824). security: "definer" needs searchPath (OKM1823). LANGUAGE sql that begins with BEGIN ATOMIC can omit dependsOn. Volatility defaults to volatile. Security defaults to invoker.
  • Triggers is a list of trigger(). Identity is the table plus the name. The function must already be in the catalog (OKM1020). updateOf is SQL column names and needs an update event at level row. Events are stored as insert, update, delete, truncate.
  • Views is a list of view() and materializedView(). Output columns are text. A tenant view that hides the key is OKM1820. refresh: "concurrently" needs a unique index (OKM1822). Creation is WITH NO DATA. db.views.name.refresh() refreshes a materialized view.
  • The Table sidebar leads with columns, keys, defaults, references, relations, and queries. Names, visibility, checks, and required rows come last.
  • Docs pages drop the breadcrumb. Copy page sits on the title row and opens Markdown and chat actions. A code block focuses the line the paragraph names.
  • Connect is a group under Define: Drivers, Target, Topology, Check, Session, Errors, Generators, and Observers. /docs/define/connect is the overview, including close(). Session is the pool size, prepare mode, TLS, search path, and deadlines. Check includes the catalog artifact and requireMeta.
  • Define Config is one page per defineConfig option. /docs/define/config is the overview.
  • The handbook adds Reference and AI Resources. Reference is CLI, Errors, OKID, and Environment Variables. AI Resources is MCP, Skills, and LLM. Retired /docs/reference/known-limits and /docs/reference/linter still redirect to the docs home.
  • The site tells agents where to read. Every response links /llms.txt, the sitemap, and /.well-known/agent-skills/index.json. A handbook URL also links its markdown page. robots.txt allows search, live AI input, and training. The published skill is okmodel. A browser with WebMCP gets search_docs, get_page, and list_pages.
  • The home page is a hero over one stage with twenty-one real examples in three groups. Migrate shows eight schema edits (add a column, require a value, add a unique key, add a foreign key, add an index, rename a column, add a picklist, drop a table), each with the schema.ts diff, the planned migration with class and lock, the lint findings, and the type change. Model shows six (timestamps, soft delete, enum, view, a tenant column, row-level security). Query shows seven calls (a relation include, a cursor page, full-text search, an aggregate, a tenant-scoped read, archived rows, a hidden column), each with the SQL it plans, the rows it returns, and the rules inspect() lists. Everything is real output, from okm generate and from the client on PGlite, regenerated by site/scripts/hero-derived.ts, and a test fails when the checked-in output drifts. Previous and next arrows, a counter, and a progress line walk the examples, the stage has a maximum height and each side scrolls when its content is taller, and the prism behind the copy takes the tint of the shown one.
  • Extensions, functions, triggers, and traits each end with Create your own. A name outside citext() and pgTrgm() is extension(name). A function is fn(). A trigger is trigger(). A schema trait is trait(name, { fields }) on the schema list.

💥 Breaking Changes

  • matches takes { match, language }. match is natural (the default), all-words, phrase, or advanced. { mode, config } is not accepted (D250).

🔥 Removed

  • Safety, Drivers, and Known limits are no longer their own pages. The reference pages are not published. /docs/known-limits redirects to the docs home.
  • Queries is no longer its own page. /docs/start/queries redirects to the Queries page under Table. Table is no longer one page. /docs/define/table is the overview, and each section is its own page.
  • Handbook pages no longer end with a Next link.

🐛 Fixed

runtime

  • insert with onConflict: { on, return: true } returns the existing row when another transaction inserts it first, and does not change that row (D246).
  • An update list of two different timestamps, dates, or byte values is not the same target (D246).
  • update with lock: "skip" matches the physical row by tableoid and ctid, so a claim does not change another partition or an inheritance child that shares the ctid. A path-parent update uses the same pair (D249).
  • Concurrent inserts into one sortable() group no longer pick the same position. The advisory lock is its own statement, and the max is read after that lock is held (D254).
  • bucket() refuses as: "__proto__", constructor, toString, and hasOwnProperty before it writes the row (D257).
  • rowNumber, rank, denseRank, and runningTotal refuse __proto__, constructor, toString, and hasOwnProperty as the result name. The error is OKM1120, names the key, and is raised before any SQL (D258).
  • find and count with an explicit search: undefined are OKM1120, the same as "" and null. A call that omits search is unchanged (D259).
  • bucket(), rowNumber, rank, denseRank, and runningTotal refuse the rest of the Object.prototype names, and then, as the result key. The error is the same OKM1120 sentence (D259).
  • sync on a tree-shaped many refuses an id list that contains the parent's own id and writes nothing. The message says "tree". manyThrough, including a row that links to itself, is accepted. Other relations are unchanged (D259).
  • A malformed advanced search is OKM1120 and names the search input. A missing text search configuration is OKM1122, including public.english. Other syntax errors stay driver (D260).
  • morphTo with a null name, a null type, or an id that is not a non-empty string is OKM1120. An empty string name or type stays OKM1020 (D260).
  • Updating a tree parent to the row's own id is OKM1120 when where is the primary key. A filter that is not the key is not checked (D260).
  • update with limit and returning: true returns the updated rows and honors select. Without it the result stays { count } (D260).
  • update with { allow, returning: true } is typed as the updated rows, and that is what the call returns.
  • delete with { returning: true } is OKM1120 and deletes nothing. The option type does not accept true. Pass the field names, or omit returning for { count }.
  • An include select that names a hidden column is OKM1120. A default include still omits the column (D260).

dialects

  • A 42601 whose message names tsquery, or whose statement is a search predicate (to_tsquery), is OKM1120. An unescaped backslash is included. Other syntax errors stay driver (D263).

adapters

  • A server error whose text contains CONNECT is no longer treated as a lost connection. Classification uses the driver code and SQLSTATE (08 and 57P01–57P03) only. A live session is rolled back and released. A session that is gone is released without a rollback (D244).
  • On PGlite, a JS number, bigint, or boolean bound to a column is sent as wire text. A parameter protocol error (08P01) is kind driver. A closed connection stays unavailable (D259).
  • Bun.sql drops a pooled connection on a closed signal or 57P01 before the next statement uses it. A read that failed because that connection was already dead is tried once on a new connection. A write, a lock, or a statement inside a transaction is not retried (D260).
  • Bun.sql rejects a wire integer outside the signed int8 range as kind invalid, including an id of 2^64. A numeric value written as that integer is refused too (D260).
  • PGlite connect(pool) then close() marks that client closed. Later calls are unavailable. The pool stays open (D260).
  • A reserved Bun.sql handle whose backend was killed fails the next statement with unavailable (SQLSTATE 08006, retryable) after the pool drains the closed socket. A scoped read does not come back empty (D263).
  • On postgres.js, a wait with no call deadline is rejected when every slot in its pool has closed, or when the reserved connection it uses closes. A queued reserve is started again after a close, because the driver drops that sql.reserve() without settling. A wait that already has a deadline is not on the close hook (D263).
  • The timestamp conformance statements set the session time zone to UTC, so the wire text does not depend on the server time zone.

tooling

  • okm migrate check does not open a second connection to drop the scratch schema when the replay rolls back (D245).
  • The replica probe test has a 15 second budget, so a loaded CI runner no longer fails it on the default 5 second limit.
  • bun run attw runs node_modules/.bin/attw from the repository root and fails with a clear error when that binary is missing.
  • bun run pack-tracked packs the tarball and fails when a file in it, other than dist/, is not tracked by git. bun run check runs it. The release publish job runs it with --release, which also fails when the worktree is dirty.
  • The docs site hero and theme control use inline SVG icons again so the client shell does not import lucide-react.
  • Streaming replication waits poll for 45 seconds before routing.auto fails on a slow catch-up.
  • The postgres.js adapter incremental gate is 16,000 minified bytes. The room is the close rule (D263).

docs

  • The docs site favicon is the OKM mark: a white letter on a dark rounded square. It cycles O, K, then M on its own icon link, so opening a page does not rewrite the static icon.
  • The favicon morph keeps the letter visible. The paths sit in the same box as the OKEngine mark, so the blur does not wipe the tile.
  • The Schema page lists extensions as citext(), pgTrgm(), or extension("name"). Config states each option once.
  • Text search: natural combines unquoted words with AND. advanced is raw to_tsquery. A syntax error in that query is OKM1120 and names the search input. A missing text search configuration is OKM1122. An empty or malformed name is still OKM1122 when the statement is built (D259, D260).
  • A claim returns { count } unless returning: true, which returns the updated rows, including when { allow } names a guarded field. delete does not accept { returning: true } (OKM1120, and the row stays). The handbook sets a claimedBy token, then finds by that token (D259, D260).
  • PGlite is a pool of one. A statement issued while a tx() is open waits until it ends, unless timeouts.acquire is set (D259).
  • A morph write can store another tenant's id. The include reads as the caller, so that target is null (D259).
  • An include select that names a hidden column is OKM1120. A default include still omits it (D260).
  • okm migrate check runs the statements it is given. Run it as a non-superuser (D259).
  • The first call after a backend kill on an idle connection is unavailable and retryable. Bun.sql retries a client read once at transaction depth 0. postgres.js and node-postgres do not. A kill during a postgres.js connect can leave an unhandled rejection from the driver's type-catalog query (D263).
  • A postgres.js wait with no call deadline is rejected on a close only when every slot in that pool has closed, or when the reserved connection it uses closes. A queued sql.reserve() is dropped by the driver without settling when a slot closes. okmodel starts it again. A wait that already has a deadline ends at that deadline (D263).
  • Docs search answers GET /api/search?query= with the matching pages. An empty query and a query with no matches return an empty list. The static index stays on the server.
  • A trigger that also fires on insert names new in when. Postgres rejects old on an insert event.
  • extension("hstore", { schema: "extensions" }) needs create schema "extensions" before apply. The column type is extensions.hstore, because that type is not on the search path.
  • The traits insert is awaited. await using closes the client when the function returns, so a returned promise closed it before the row was written.
  • The docs site reads the repository package.json by walking up to the manifest named okmodel. A Next output manifest with a trailing comma is not that file.

♻️ Changed

dialects

  • A uuidv7() default on a floor below 18 is OKM1812 and names that major. A server below minPostgres is OKM1802. A server below 15 with no floor is OKM1803. The handbook page is Minimum Postgres (D261).
  • A bare t.id() stores gen_random_uuid(), so it works on Postgres 15 to 18. defaults: { id: "uuidv7" } keeps the old database default. An explicit uuidv7() on a server below 18 is still OKM1812 and needs minPostgres: 18. An existing table whose default is uuidv7() plans SET DEFAULT gen_random_uuid() and does not rewrite the table (D262).

runtime

  • update of a list refuses the same target twice (OKM1120). A target that matches nothing is not an error. An empty set is OKM1120. An empty list runs no statement (D246).
  • inc is a number or bigint column. A text column is OKM1124. A list that sets one field with inc in one row and a plain value in another is OKM1120. A null number stays null (D249).
  • .stream() yields rows in keyset batches on every driver, including PGlite. The default batch is 1000. limit is the total. A table with no primary key, one(), count(), exists(), search, and a window are refused. A positive timeout is the budget for the whole stream, including on a driver that cannot cancel. A row committed while it runs can be seen or missed (D256).

adapters

  • A closed pool reports SQLSTATE 08006 with the message The pool is closed.

tooling

  • The size gates for the featureless app and the connect entries measure the static startup graph. Dynamic import targets are empty modules in that build (D248).
  • The docs site has no sync step. site/content/docs is the handbook and is checked in. Each page carries its title and description in frontmatter, and each folder's meta.json sets the sidebar order. site/handbook, scripts/sync-docs.ts, and lib/docs-map.ts are removed. The site check now verifies that every page is listed in its meta.json.
  • The docs site deploys to production only after a release is published. A push to main no longer updates the public site, so the site shows the published version.
  • The docs site publishes when the workflow moves the release branch to the release commit. Vercel builds that branch. The workflow no longer calls the Vercel CLI, and the repository no longer needs VERCEL_TOKEN, VERCEL_ORG_ID, or VERCEL_PROJECT_ID.
  • A release requires the commit that was QA'd. The tag must point at that commit, package.json there must match the tag, and the published provenance must record that same commit.

docs

  • The docs site uses IBM Plex Sans.
  • The site header shows the OKM wordmark. The home link is named OKModel.
  • The header wordmark is one step smaller. The theme control uses the Lucide moon and sun.
  • The home page hero reads "Edit your schema. Get a safe migration." with a pitch for the migration, lint, and types the stage shows. The package description is unchanged.
  • Known limits record the lost-connection release path and that a sortable move is O(n) per group.
  • The changelog page shows ## Unreleased as next until the release.
  • The published README is the short page: the logo, install, a schema, okm push, and basic usage. The full walkthrough is docs/guide.md. A README image has to be an absolute https:// URL. The docs site is https://okm.omqkhafi.dev.
  • The spec status names the 0.7.0 release. Decisions run through D264.

⚠️ Deprecated

  • requires: { postgres: ">=17" } still sets the floor and is deprecated. Use minPostgres (D261).