v0.7.0
✨ Added
dialects
minPostgres: 17sets the oldest Postgres major this schema runs on. The majors are 15, 16, 17, and 18. Omitted means 15.requires: { postgres: ">=17" }stores the same floor. The floor is not part of the catalog hash (D261).
runtime
bucket(column, unit, { tz, as })groups an aggregate by a calendar or clock unit. A day or longer returnsTemporal.PlainDate. On atimestamptz, a second, minute, or hour returnsTemporal.Instant. Those three units are refused on adate. A NULL timestamp is one group: the result key is present and the value is null.__proto__,constructor,toString, andhasOwnPropertyare refused asas, and so is anasthat matches another bucket. There is still nohaving. Time-zone date helpers stay deferred (D247, D257).updatewithlock: "skip"andlimitclaims up to that many unlocked rows in one statement. A read still useslock: "update"andwait: "skip"insidetx()(D249).find({ search })andcount({ search })match a column built for search. A string is enough when the table has one. Rows come back closest first unless you passorderBy.page()does not takesearch. Alanguagethat is not text is refused. An empty or malformed language name is OKM1122. A well-formed name is sent to the server (D250).relevance(query, { match, language })orders a search column by the match.matchestakes the same options.{ mode, config }is not accepted (D250).- A to-many
includewithorderByandlimitreturns the first N related rows of each parent. The primary key breaks ties (D251). rowNumber,rank,denseRank, andrunningTotalgo inselectonfindorone. A running total is the column's type, or null.page()refuses a window when the call runs (D251).descendants(id)andancestors(id)read the rows below or above a row in one statement. Passrelationwhen the self-relations use different columns. The fix lists the names. A row already on the path is not entered again.ltreeis not required (D252).sync(id, relation, ids)sets a to-many relation to exactly those ids and reports the links it added and removed (D252).versioned()addsversion. An update passes the version you read and the write bumps it. A stale write is a conflict and changes nothing. Leavingversionout is refused, and so isversionon a table that is not versioned (D254).sortable(groupBy?)addspositionandmove(id, { before })or{ after }. New rows go at the end of the group. A move is O(n) per group and rewrites every row in the group. Naming the group column insetis refused.findandpageorder bypositionwhen you omitorderBy(D254).morph("commentable", ["tasks", "lists"])includes the matching target, with the type set to that table's name, or null.morphManyis the reverse many.morphTosets the type and the id together. Setting one without the other is OKM1121. A type outside the list is OKM1120 (D255).
docs
- The handbook documents those calls, including what each one refuses. Define is Schema, Connect, and Config. Start is Let's start and Basic Usage. Table is a group: one page per part of
table(), from the key through the queries. Keys, Checks, Presets, and Reference rows are split into subsections. - Traits shows
trait(name, { fields }).touchseals those columns and sets them tonow()on update.methodsandrequiresstay OKM1061. Each built-in trait shows the call: insert for timestamps,archiveandrestorefor archivable,versionon update, andmove. - Defaults shows what insert does with each form. A literal and
.defaultSql()are stored and applied when the field is omitted. A generator runs in the application and stays out of the catalog.t.id()andt.identity()are omitted from input..default(null)follows.nullable(). A generated column isstored. - Shape shows the value you pass. An array element cannot be null. A picklist CHECK is
column IN (...), and{ check: false }still rejects a value at encode. Adding an enum label in order plansALTER TYPE … ADD VALUE. Removing or reordering a label replaces the type. - Names: insert and
whereuse the field name.sqlNameis what the statement quotes, and casing does not rewrite a name you set.renamedFromplansRENAME. A comment is checked and left off the catalog. - Visibility splits the options.
.hidden()leaves the column off the row.filterable: trueopenswhereandorderByat runtime..sensitive()takes no argument and redacts logs, errors, andinspect()as[redacted]..guarded()stays closed until{ allow }. A sealed field and a tenant key stay closed. A tenant key update is OKM1704. - Validation splits the options, and each option has a sample.
v.trim()andv.lowercase()take no argument and change the stored string.v.email,v.min, andv.ruletake an issue key. A bareonInsert()oronUpdate()limits the rules that follow. Passing one rule limits that rule and leaves the rest of the list alone.validation: truekeeps the schema'sonReadandstyle.falseturns the table off.insert.validatereturns a frozen row.insert.checkreturns the issues.pickandomitchoose the fields.{ validate: false }skips that call. - References splits each action. The constraint is
{table}_{column}_fkey. A new table adds it withalter table. A table that already exists adds itnot valid, then validates it.onDeleteandonUpdatetakecascade,no action,restrict,set default, orset null, and both can be set together.set nullneeds a nullable column. On a tenant table,set nullandset defaultare refused. Omittedcolumnsuses a one-column primary key. One name selects another column, which has to be the primary key or a unique. - Relations names the include.
oneandmanytakefieldwhen more than one foreign key matches. A to-many include needslimit.has,none, andeveryfilter a relation.manyThroughtakesthrough,from, andto.morphuses${name}Typeand${name}Id. Include istrue.morphTosets both columns. - Indexes take column handles in order. One column, a column order, and several indexes are separate calls. The name is
{table}_{columns}_idxand follows a column or tablesqlNameand snake casing. A new table usescreate index. A table that already exists usescreate index concurrently, outside a transaction..unique()can sit beside a plain index and does not create a unique constraint. - Checks take a name key and
sqltext. The constraint is{table}_{key}_check. The key is not recased. A handle is inserted as its SQL name. A number is inserted as text. Quotes around a string stay in the template. A new table usesalter table. A table that already exists adds the checknot valid, then validates it. A failed row isCheck violationwith kindcheck. filters().parsereturns{ where, orderBy }forfind. Each operator is its own section. A scalar is equality. Several operators becomeand.inandinListbecomeinList.sortis ascending. A relation field becomeshas, and a column of the same name wins. A simple request is one field and a sort. A complex request combines operators, a relation, andsort, then spreads intofind. A hidden field is OKM1123 whenfilters()is called.trait()takesfields,touch,sealed, andpresets. A touched field is sealed. Schema traits apply first.omitDefaultsdrops them and needs a reason. A repeated column is OKM1012. A simple trait adds one field. A complex trait adds a touched column, a sealed column, and a preset.- Tenancy adds a guarded uuid.
for()sets it. Input cannot. A unique includes the key unless.unique({ global }).global("reason")leaves the column off.via()follows up to three to-one relations and does not store the key. A global table cannot reference a tenant table. - A preset is a
whereyou call on the table. A field, an argument, a list, a bound,or, and twowherecalls are separate sections. A chain keeps the caller'swherefirst. Update and delete still need their ownwhere. Insert does not use the preset. A reserved name is OKM1040. A trait preset sees the trait's columns. - Required rows are the
referenceoption. They are rows the application expects, such as statuses. Apply inserts a missing key and does not update or delete. They are not a foreign key and not a migration insert. - Queries add
page, a groupedaggregate, a stringsearch, both tree walks, a listupdate, andinc. A windowasthat clashes with a column is OKM1120.page()still readsselectas field names. - Each table option is its own subsection with a sample.
t.id()showsuuidv7,uuidv4, a client generator, andnone. A tenant unique leaves the key out with{ global: "reason" }.has,none, andeveryeach filter a relation.onConflictis"ignore",{ on, update }, or{ on, return: true }. - Schema is a group under Define. Each option is a page: Tables, Casing, Codecs, Row types, Id default, Minimum Postgres, Traits, Tenancy, Validation, Extensions, Functions, Triggers, and Views.
/docs/define/schemais the overview./docs/define/schema/requiresredirects to Minimum Postgres. - Tables is the list
schema()compiles. A reference names a table in that list, earlier or later. The same name twice, two names that emit one type, or one SQL name twice is OKM1023. A missing name is OKM1020. Swapping the list does not change the catalog hash. - Casing
"snake"rewrites a camelCase table or column.publishedAtispublished_at.userIDisuser_i_d.sqlNameandrenamedFromstay as written. Any other value isschema() casing … must be snake. - Codecs choose the TypeScript value of
t.bigint(),t.identity(), andt.numeric(). The SQL type stays."string"is a decimal string."number"is a safe integer for bigint and can lose digits on numeric."bigint"is a JavaScript bigint. A schema codec other than"string"needs the sameason every column of that type. Timestamps stay"temporal". - Row types store
"emitted"or"inferred". Omitting the field is"emitted". Queries still infer fromschema().okm buildwrites.okm/types.d.tsfor either value.notesbecomesNotes,NotesInsert, andNotesUpdate. At.jsonb<T>()argument is copied from the schema source. Any other value isschema() types … must be emitted or inferred. - Id default replaces a bare
t.id(). Omitted and"uuidv4"storegen_random_uuid()."uuidv7"storesuuidv7()and needs Postgres 18. A client generator is filled on insert and stays out of the catalog.okid()istextwith collationC. A columndefaultwins."none"is that column option. A Postgres floor below 18 onuuidv7()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 baret.id()storesgen_random_uuid(). An explicituuidv7()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.omitDefaultsdrops the schema list and needs a reason. A repeated column is OKM1012.timestamps({ enforce: "trigger" })addsokm_touch_<column>and a before-update trigger. - Tenancy is
columnTenancy(),compositeTenancy(), orrlsTenancy(). Omitted, a table that sets tenancy is OKM1060. Each key is a guarded uuid filled byfor().rlsTenancykeeps the column predicate and refuses the owner, a superuser, andBYPASSRLS(OKM1707). - Validation is stored as given.
schema()does not run the rules. Omitted andfalseare off.trueand an object are on.styleisinlineorsection. A field in both places is OKM1030. A write withoutokmodel/validateis OKM1201. - Extensions is a list of
citext(),pgTrgm(), orextension("name"). The record is contributed when the catalog is read. An omittedcitextcolumn 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 needsdependsOn(OKM1824).security: "definer"needssearchPath(OKM1823).LANGUAGE sqlthat begins withBEGIN ATOMICcan omitdependsOn. Volatility defaults tovolatile. Security defaults toinvoker. - Triggers is a list of
trigger(). Identity is the table plus the name. The function must already be in the catalog (OKM1020).updateOfis 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()andmaterializedView(). Output columns are text. A tenant view that hides the key is OKM1820.refresh: "concurrently"needs a unique index (OKM1822). Creation isWITH 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/connectis the overview, includingclose(). Session is the pool size, prepare mode, TLS, search path, and deadlines. Check includes the catalog artifact andrequireMeta. - Define Config is one page per
defineConfigoption./docs/define/configis 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-limitsand/docs/reference/linterstill 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.txtallows search, live AI input, and training. The published skill isokmodel. A browser with WebMCP getssearch_docs,get_page, andlist_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.tsdiff, 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 rulesinspect()lists. Everything is real output, fromokm generateand from the client on PGlite, regenerated bysite/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()andpgTrgm()isextension(name). A function isfn(). A trigger istrigger(). A schema trait istrait(name, { fields })on the schema list.
💥 Breaking Changes
matchestakes{ match, language }.matchisnatural(the default),all-words,phrase, oradvanced.{ 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-limitsredirects to the docs home. - Queries is no longer its own page.
/docs/start/queriesredirects to the Queries page under Table. Table is no longer one page./docs/define/tableis the overview, and each section is its own page. - Handbook pages no longer end with a Next link.
🐛 Fixed
runtime
insertwithonConflict: { 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).
updatewithlock: "skip"matches the physical row bytableoidandctid, so a claim does not change another partition or an inheritance child that shares thectid. A path-parent update uses the same pair (D249).- Concurrent inserts into one
sortable()group no longer pick the sameposition. The advisory lock is its own statement, and the max is read after that lock is held (D254). bucket()refusesas: "__proto__",constructor,toString, andhasOwnPropertybefore it writes the row (D257).rowNumber,rank,denseRank, andrunningTotalrefuse__proto__,constructor,toString, andhasOwnPropertyas the result name. The error is OKM1120, names the key, and is raised before any SQL (D258).findandcountwith an explicitsearch: undefinedare OKM1120, the same as""andnull. A call that omitssearchis unchanged (D259).bucket(),rowNumber,rank,denseRank, andrunningTotalrefuse the rest of theObject.prototypenames, andthen, as the result key. The error is the same OKM1120 sentence (D259).syncon a tree-shapedmanyrefuses 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 staydriver(D260). morphTowith 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
whereis the primary key. A filter that is not the key is not checked (D260). updatewithlimitandreturning: truereturns the updated rows and honorsselect. Without it the result stays{ count }(D260).updatewith{ allow, returning: true }is typed as the updated rows, and that is what the call returns.deletewith{ returning: true }is OKM1120 and deletes nothing. The option type does not accepttrue. Pass the field names, or omitreturningfor{ count }.- An include
selectthat names a hidden column is OKM1120. A default include still omits the column (D260).
dialects
- A
42601whose message namestsquery, or whose statement is a search predicate (to_tsquery), is OKM1120. An unescaped backslash is included. Other syntax errors staydriver(D263).
adapters
- A server error whose text contains
CONNECTis no longer treated as a lost connection. Classification uses the drivercodeand SQLSTATE (08and57P01–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 kinddriver. A closed connection staysunavailable(D259). - Bun.sql drops a pooled connection on a closed signal or
57P01before 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)thenclose()marks that client closed. Later calls areunavailable. The pool stays open (D260). - A reserved Bun.sql handle whose backend was killed fails the next statement with
unavailable(SQLSTATE08006, 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
reserveis started again after a close, because the driver drops thatsql.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 checkdoes 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 attwrunsnode_modules/.bin/attwfrom the repository root and fails with a clear error when that binary is missing.bun run pack-trackedpacks the tarball and fails when a file in it, other thandist/, is not tracked by git.bun run checkruns 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.autofails 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(), orextension("name"). Config states each option once. - Text search:
naturalcombines unquoted words with AND.advancedis rawto_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 }unlessreturning: true, which returns the updated rows, including when{ allow }names a guarded field.deletedoes not accept{ returning: true }(OKM1120, and the row stays). The handbook sets aclaimedBytoken, thenfinds by that token (D259, D260). - PGlite is a pool of one. A statement issued while a
tx()is open waits until it ends, unlesstimeouts.acquireis 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
selectthat names a hidden column is OKM1120. A default include still omits it (D260). okm migrate checkruns the statements it is given. Run it as a non-superuser (D259).- The first call after a backend kill on an idle connection is
unavailableand 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
newinwhen. Postgres rejectsoldon an insert event. extension("hstore", { schema: "extensions" })needscreate schema "extensions"before apply. The column type isextensions.hstore, because that type is not on the search path.- The traits insert is awaited.
await usingcloses the client when the function returns, so a returned promise closed it before the row was written. - The docs site reads the repository
package.jsonby walking up to the manifest namedokmodel. 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 belowminPostgresis OKM1802. A server below 15 with no floor is OKM1803. The handbook page is Minimum Postgres (D261). - A bare
t.id()storesgen_random_uuid(), so it works on Postgres 15 to 18.defaults: { id: "uuidv7" }keeps the old database default. An explicituuidv7()on a server below 18 is still OKM1812 and needsminPostgres: 18. An existing table whose default isuuidv7()plansSET DEFAULT gen_random_uuid()and does not rewrite the table (D262).
runtime
updateof a list refuses the same target twice (OKM1120). A target that matches nothing is not an error. An emptysetis OKM1120. An empty list runs no statement (D246).incis a number or bigint column. A text column is OKM1124. A list that sets one field withincin 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.limitis the total. A table with no primary key,one(),count(),exists(),search, and a window are refused. A positivetimeoutis 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
08006with the messageThe 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/docsis the handbook and is checked in. Each page carries its title and description in frontmatter, and each folder'smeta.jsonsets the sidebar order.site/handbook,scripts/sync-docs.ts, andlib/docs-map.tsare removed. The site check now verifies that every page is listed in itsmeta.json. - The docs site deploys to production only after a release is published. A push to
mainno longer updates the public site, so the site shows the published version. - The docs site publishes when the workflow moves the
releasebranch to the release commit. Vercel builds that branch. The workflow no longer calls the Vercel CLI, and the repository no longer needsVERCEL_TOKEN,VERCEL_ORG_ID, orVERCEL_PROJECT_ID. - A release requires the commit that was QA'd. The tag must point at that commit,
package.jsonthere 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
## Unreleasedas next until the release. - The published README is the short page: the logo, install, a schema,
okm push, and basic usage. The full walkthrough isdocs/guide.md. A README image has to be an absolutehttps://URL. The docs site ishttps://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. UseminPostgres(D261).