Errors

OkmError carries a kind, a category, and a code. okm doctor prints each OKM code.

A failure from the client is an OkmError. Import the class from okmodel.

catch.ts
import { OkmError } from "okmodel";

export function respond(error: unknown) {
  const err = OkmError.from(error); 
  return err.toHttp();
}

code is an OKM1xxx guard, or the kind name when that kind has no number. kind selects the category. summary is the same text as message.

Fields

FieldWhat it holds
codeOKM1xxx, or the kind when that kind has no number.
kindOne of the kinds below.
categoryThe category of that kind.
summaryWhat failed. Same string as message.
retryableWhether a declared retry may run the call again.
fix{ summary, suggestion? }. suggestion is a nearer accepted name.
tableTable the database named, when it named one.
columnsColumns the failure names. Empty when it names none.
constraintConstraint name the database sent.
sqlstateSQLSTATE the database sent.
batchIndexStatement index inside a batch, or null on one statement and at commit.
issuesValidation checks. Present on OKM1200.

fields() maps each column to a reason. The same object comes back on later calls. It has no row values.

values() returns row values only when the error was built with includeValues. Errors on connect is where that flag is set. log() returns the structured record: name, code, kind, category, summary, retryable, batch index, fix, fields, and table, columns, constraint, SQLSTATE, and issues when they are set. It leaves the row values out, and so does toHttp(). The logger on the client receives { code, summary }.

toHttp() returns { status, body }. The body is { code, reason, fields }. reason is the summary. A category missing from the status map keeps the default in the table below.

match runs the handler for this category. _ runs when that category is omitted.

match.ts
import { OkmError } from "okmodel";

export function formOrHttp(error: OkmError) {
  return error.match({
    input: (item) => item.fields(), 
    conflict: (item) => item.fields(), 
    _: (item) => item.toHttp(), 
  });
}

OkmError.is narrows the kind, and the table when you pass one.

unique.ts
import { OkmError } from "okmodel";

export function emailTaken(error: unknown): boolean {
  return OkmError.is(error, "unique", "notes"); 
}

OkmError.from returns an OkmError unchanged. A DriverError whose kind is timeout, cancelled, or outcome_unknown keeps that kind. Any other thrown value becomes kind driver and code driver. from does not read constraint SQLSTATEs. The client does that before a query rejects.

A query exposes the same settlement as safe from okmodel.

insert.ts
import { db } from "./db.ts";

export async function addNote(title: string) {
  const result = await db.notes.insert({ title }).safe(); 
  if (!result.ok) return result.error.summary;
  return result.value;
}

{ ok: true, value } is success. { ok: false, error } is the mapped OkmError.

Row values

The summary drops =(...) from a server message, and turns contains (...) into contains (…). With includeValues: true, values() still returns the row. The log line does not.

Kinds

ERROR_KINDS and ERROR_CATEGORIES are exported from okmodel. The constructor derives the category from the kind. A transient kind retries, except cancelled. outcome_unknown is internal, and it does not retry.

KindCategoryStatusRetries
invalidinput422no
not_nullinput422no
checkinput422no
foreign_keyinput422no
uniqueconflict409no
exclusionconflict409no
conflictconflict409no
not_foundnot_found404no
not_uniquenot_found404no
forbiddenforbidden403no
serializationtransient503yes
deadlocktransient503yes
lock_timeouttransient503yes
timeouttransient503yes
cancelledtransient503no
unavailabletransient503yes
schema_driftinternal500no
driverinternal500no
read_onlyinternal500no
outcome_unknowninternal500no

Pass http on connect, or pass statuses to toHttp(statuses), to replace one of these. A category you leave out keeps the status in the table. The connect page is Errors.

When the constructor is given no fix, the kind supplies one sentence.

KindsDefault fix
cancelledDo not retry. The caller aborted the call.
outcome_unknownCheck whether the write landed, using an idempotency key, before retrying.
timeout, serialization, deadlock, lock_timeout, unavailableRetry the call.
unique, exclusion, conflictChange the conflicting value, or update the existing row.
not_nullProvide a value for the column.
check, invalidChange the value so it passes the check.
foreign_keyUse a key that exists, or insert the referenced row first.
not_foundLook up a row that exists.
not_uniqueNarrow the filter so it matches one row.
forbiddenUse a role that is allowed to perform this operation.
read_onlyRun the write on the primary.
schema_driftApply the migrations that match this build.
driverRead the summary and correct the call.

A fix passed in replaces that sentence. A rejected name that is close to an accepted one appends a did-you-mean hint to the summary and sets fix.suggestion to that name.

Which code, which kind

Three codes pick a kind when the caller does not pass one.

CodeKind
OKM1401outcome_unknown
OKM1846timeout
OKM1520schema_drift

A code that is itself a kind name uses that kind. A mapped database error does this for every kind except outcome_unknown. The client also raises invalid, not_found, not_unique, timeout, cancelled, unavailable, and forbidden with the kind as the code. Every other OKM1xxx, with no kind passed, is invalid.

Four codes pass a kind other than that default.

CodeKind
OKM1522conflict
OKM1825forbidden
OKM1850forbidden
OKM1851forbidden

Driver failures

The adapter normalises the driver exception into DriverError. postgres.js and node-postgres put the SQLSTATE on code. Bun.sql puts ERR_POSTGRES_SERVER_ERROR on code and the SQLSTATE on errno. The copied fields are sqlstate, constraint (constraint_name or constraint), table (table_name or table), column (column_name or column), detail, and batchIndex.

cancelled() is the caller's signal. timedOut() is a statement, transaction, or batch deadline. outcomeUnknown() is a commit whose result never arrived. Those three stay DriverError until the client maps them. acquireTimeout() is already OkmError OKM1846. A closed pool is a DriverError with SQLSTATE 08006.

The client maps that error. An OkmError passes through. A driver kind of timeout, cancelled, or outcome_unknown wins over the SQLSTATE. 57014 without one of those kinds is timeout, because cancelled comes only from the signal.

SQLSTATEKind
23505unique
23502not_null
23514check
23503, 23001foreign_key
23P01exclusion
23000conflict
40001serialization
40P01deadlock
55P03lock_timeout
57014timeout
40003outcome_unknown
25006read_only
class 08, 53300, 57P01, 57P02, 57P03unavailable
class 22invalid
class 53unavailable

Anything else is kind driver. With no SQLSTATE, a TLS failure or a dropped connection is unavailable.

The connection codes are ECONNRESET, ECONNREFUSED, EPIPE, ENOTFOUND, ETIMEDOUT, CONNECT_TIMEOUT, EHOSTUNREACH, ENETUNREACH, EAI_AGAIN, CONNECTION_CLOSED, CONNECTION_ENDED, CONNECTION_DESTROYED, and ERR_POSTGRES_CONNECTION_CLOSED. Classification reads the code, the errno, or the SQLSTATE. It does not read the message. A TLS cause starts with ERR_TLS_, ERR_POSTGRES_TLS_, or SELF_SIGNED_CERT_, or is exactly DEPTH_ZERO_SELF_SIGNED_CERT or UNABLE_TO_VERIFY_LEAF_SIGNATURE.

These SQLSTATEs keep their kind and add a reason.

SQLSTATEKind staysReason
42P01driverundefined_table
42703driverundefined_column
0A000, message contains materialized view is not populateddrivernot_populated
55000, message contains cannot refresh materialized view and concurrentlydriverno_unique_index
42501, message contains materialized viewdrivernot_owner
no SQLSTATE, TLS causeunavailabletls

fields() uses that reason. With a schema, the reason is the catalog object's name key for the columns the error names. With no schema, table_pkey becomes pkey, and a trailing _fkey, _check, or _key is removed. No constraint name leaves the kind as the reason.

Bun.sql and PGlite reject a positive timeout as OKM1120. The message is timeout. The kind is invalid. They cannot cancel a statement that is already running.

Doctor

okm doctor OKM1020 prints the registry entry: the code, the title, the summary, and the fix.

okm doctor OKM1020
OKM1020: Unknown table
A table, dependency, or catalog object is missing or does not match its identity.
Use a name that is in the schema. The error lists the accepted names and a nearest-name hint when one is close.

An unknown code throws OkmError with code invalid. The message is Unknown code, then the argument, then a period. The fix says to pass a code from the registry, or to run okm doctor with no argument.

With no code, the command lists each table's triggers, one line per table, or no triggers. When the config sets roles, it also connects and checks that those roles exist, that a managed role can be created, and that the application role can reach each managed object. The command does not change the database.

The registry is not on the runtime entry. A process that never explains a code does not load it.

Registry

The registry has 110 codes, in numeric order. Ranges are schema OKM1000, queries and capabilities OKM1100, validation OKM1200, mapped database errors OKM1300, transactions OKM1400, migrations OKM1500, typed SQL OKM1600, tenancy and policies OKM1700, drivers and versions OKM1800, and CLI OKM1900. Mapped database errors use the kind as the code, so OKM1300 has no entry. OKM1900 has no entry. The tables below are every code the registry holds.

Schema

CodeTitleSummary
OKM1012Trait field conflictA trait added a field that the table already declares.
OKM1020Unknown tableA table, dependency, or catalog object is missing or does not match its identity.
OKM1021Foreign key targetA foreign key is missing its target table or column, or the target is ambiguous.
OKM1022Foreign key typeA foreign key column and the column it references have different types.
OKM1023Duplicate nameTwo catalog objects share an identity, or two tables emit the same type name.
OKM1024Table file not in schemaA table file is not passed to schema().
OKM1025Multiple RegisterMore than one Register augmentation is in the project.
OKM1026Dependency cycleThe catalog dependency graph contains a cycle, so objects cannot be ordered.
OKM1027Unreadable catalogThe catalog document is malformed or uses a version this build cannot read.
OKM1030Validation in two placesA field is validated both on the column and somewhere else.
OKM1040Preset name collisionA preset uses the name of a client method or a reserved name, or a table and its traits (or two traits) define the same preset.
OKM1051Archive foreign keyA foreign key into a table-strategy archivable table uses CASCADE, SET NULL, or SET DEFAULT and is not listed in cascade.
OKM1052Not archivablearchive or restore was called on a table that is not archivable.
OKM1060Invalid columnA column definition has a bad length, precision, scale, array rank, interval qualifier, or an empty or repeated picklist or enum.
OKM1061Reserved optionThe option exists in the types but this version does not implement it.

Queries and capabilities

CodeTitleSummary
OKM1101Find without limitfind was called without a limit.
OKM1102Unfiltered writeA write with no effective predicate, including undefined values, is OKM1102.
OKM1104Upsert targetonConflict names columns that are not a unique constraint.
OKM1105Include without limitA to-many include has no limit.
OKM1110Engine versionA feature needs a newer PostgreSQL than minPostgres allows. The message names the feature, the version it needs, and this schema's minimum.
OKM1111Missing capabilityThe call needs a driver capability this driver does not have.
OKM1120Unknown fieldThe name is not a field or an option on a write, read, or update. An unknown key on insert, update, upsert, or first-or-create is refused and names the key and the table. timeout and signal go in the second argument of update. Options go first: tx(options, fn).
OKM1121Object as valueAn empty or() branch is OKM1121. or() takes one array: or([a, b]). A plain object was passed where a value is required.
OKM1122Bad identifierAn identifier is empty, too long, contains NUL or another control character, or is an unquoted reserved word.
OKM1123Hidden field allowlistA hidden field was added to a filter or sort allowlist.
OKM1124Operator mismatchAn operator does not apply to the column type.
OKM1125Bad SQL fragmentA composed fragment is empty, unbalanced, includes a backslash, or interpolates a value it cannot bind. A select fragment that is not a number is the same code.
OKM1126Parameter limitThe finished statement has more than 65,535 parameters. The limit is for the whole statement (filters, inList, limit, values), not for inList alone.
OKM1130Cursor orderA cursor was reused with a different orderBy than the one that created it, or is not a cursor page() returned.
OKM1190Safety violationA query composition broke a core invariant. violations lists every broken rule in stable order.
OKM1191No snapshot planA multi-statement read was requested inside READ COMMITTED and no single-statement plan exists.

Validation

CodeTitleSummary
OKM1200Validation failedA write or a standalone check failed a validation rule. issues lists each path and message key.
OKM1201Validation not importedValidation is enabled but okmodel/validate was not imported.
OKM1210Codec rejected a valueA codec refused a value: not numeric text, not a valid temporal value, or the wrong shape.

Transactions

CodeTitleSummary
OKM1401Outcome unknownA commit or batch was sent and its result never arrived, so the write may or may not have happened.

Migrations

OKM1538, OKM1539, OKM1540, OKM1543, OKM1544, OKM1545, and OKM1552 are warnings. A warning is printed and the command still succeeds. An error blocks plan, check, and apply.

CodeTitleSummary
OKM1510Unsafe migrationA migration has an unresolved lint error, or an override with an empty reason or a code no statement in the step triggered.
OKM1511Drop tableA statement drops a table. The rows and anything that reads them are gone.
OKM1512Drop columnA statement drops a column. Stored values and the code that reads them are gone.
OKM1513Drop enumA statement drops an enum that columns or code may still depend on.
OKM1514Drop domainA statement drops a domain that columns or code may still depend on.
OKM1515Drop functionA statement drops a function that queries or triggers may still call.
OKM1516Drop viewA statement drops a view that queries may still read.
OKM1517Drop extensionA statement drops an extension. A dependent still in the catalog is refused before this finding.
OKM1518Drop materialized viewA statement drops a materialized view and the rows it stores.
OKM1519Rename columnA statement renames a column. Existing queries still use the old name.
OKM1520Schema driftThe database has drifted past what an expand migration can repair.
OKM1521Snapshot mismatchProvisioning from the snapshot does not match the replayed migration history.
OKM1522Apply lock heldAnother apply already holds the target's migration lock.
OKM1523Rename tableA statement renames a table. Existing queries still use the old name.
OKM1524Column type changeA statement changes a column type. Existing readers and writers may not accept the new type.
OKM1525Required columnA statement adds a NOT NULL column with no default to a table that already has rows.
OKM1526Drop defaultA statement removes a column default. Writers that omitted the column will fail.
OKM1527Shrink lengthA statement shortens a character length. Values longer than the new limit no longer fit.
OKM1528Unique constraintA statement adds a unique or primary-key constraint on a table that already has rows.
OKM1529Unique indexA statement creates a unique index on a table that already has rows.
OKM1530Ambiguous renameA rename could match more than one object.
OKM1531Validating checkA statement adds or validates a check against rows already stored.
OKM1532Validating foreign keyA statement adds or validates a foreign key against rows already stored.
OKM1533Narrowing typeA statement narrows a numeric or integer type. Values outside the new range no longer fit.
OKM1534Index locksA statement creates an index on an existing table without CONCURRENTLY.
OKM1535Check without NOT VALIDA statement adds a check on an existing table without NOT VALID.
OKM1536Foreign key without NOT VALIDA statement adds a foreign key on an existing table without NOT VALID.
OKM1537Set not nullA statement sets NOT NULL on a column that already exists.
OKM1538Type rewriteA statement changes a column type in a way that rewrites the table. Binary-coercible changes, varchar widening, varchar to text, and a numeric precision increase at the same scale do not. The expand/contract recipe is type change.
OKM1539Timestamp without time zoneA column is timestamp without time zone. The instant depends on the session time zone.
OKM1540varchar lengthA column is varchar(n). text stores the same values without a length limit in the type.
OKM1541Picklist removalA picklist or enum value was removed and the replacement was not given.
OKM1542Data statementA migration contains a data statement outside backfill(), or a DO or CALL whose body is not analysed.
OKM1543serial columnA column uses serial or a nextval default. Identity is the form this repo generates.
OKM1544json columnA column is json. jsonb is available and stores the same documents in binary.
OKM1545Identity not alwaysAn identity column is generated by default. The repo default is generated always.
OKM1546Backfill keyA backfill step targets a table that has no primary key.
OKM1547History differsReplaying a migration does not match its stored catalog, or the file was generated from a different parent than the previous migration.
OKM1548Previous catalogAn expand migration drops or changes a table, column, constraint, or type the previous catalog relies on.
OKM1549Stale headThe last migration's catalog does not match the schema.
OKM1550Row-level security weakenedA statement disables row-level security, drops or widens a policy, grants BYPASSRLS, or changes the owner of a tenant table.
OKM1551Transaction control in a stepA step contains COMMIT, ROLLBACK, or other transaction control, including inside a DO body. A function or procedure body is not this.
OKM1552Reprinted declarationA domain check or a LANGUAGE sql function body is stored differently from the text in the migration catalog.

Typed SQL

CodeTitleSummary
OKM1601Stale typed SQLA typed SQL signature no longer matches the database. Recording signatures with okm build is deferred.
OKM1602Bad typed SQLA raw statement is missing its class, is not one select or one write, or the result class has no column initializers.

Tenancy and policies

CodeTitleSummary
OKM1701Tenant context missingA tenant table was used on a client that has no tenant context.
OKM1702Unverifiable tenant SQLRaw SQL on a tenant table cannot prove the tenant predicate. A raw write is this. A raw read that does not select the primary key with ident is this.
OKM1704Tenant key changeAn update tried to change the tenant key.
OKM1705Global references tenantA global table references a tenant table.
OKM1706Tenant indexAn index on a tenant table does not lead with the tenant key.
OKM1707RLS roleA scoped call under rls ran as a role that owns the tenant tables, is a superuser, or has BYPASSRLS. The reason field says which. The message names the endpoint.
OKM1708RLS policyA scoped call under rls found a tenant table whose row-level security is missing, not forced, or whose policies are not the ones okm generate emits. The message names the table and the endpoint.

Drivers and versions

CodeTitleSummary
OKM1801Dialect mismatchThe schema dialect and the driver dialect are not the same.
OKM1802Server versionThe connected server is older than this schema's minPostgres.
OKM1803Postgres floorThe connected server is older than PostgreSQL 15.
OKM1810Extension not declaredAn extension builder was used but the extension is not declared on the schema.
OKM1811Extension unavailableA declared extension is not installed on the server.
OKM1812Feature versionA feature needs a newer PostgreSQL than minPostgres, or an extension version the server does not have. The message names the feature and the version.
OKM1813Extension defined twiceThe same extension definition was registered twice.
OKM1814Extension change refusedThe plan would downgrade an extension, move a non-relocatable one, or drop one that still has dependents.
OKM1820View tenancyA view over tenant tables does not expose the tenant key and is not marked global.
OKM1821Replace needs recreateAn incompatible replace would have to recreate dependents. CASCADE is never used.
OKM1822Concurrent refreshREFRESH CONCURRENTLY was declared without a unique index on the materialized view.
OKM1823Security definer pathA SECURITY DEFINER function has no search_path.
OKM1824Function dependenciesA plpgsql function has no dependsOn. Postgres does not record dependencies inside the body.
OKM1825Missing privilegeA role lacks a privilege on a managed object.
OKM1830Lock outside transactionfind was called with a row lock outside tx().
OKM1840Replica not allowedA replica route was used on an operation that requires the primary, or inside tx().
OKM1841External object differsAn external object does not match the owner's exported catalog.
OKM1842Cross-database foreign keyA foreign key points at a table in another database.
OKM1843No eligible replicaA replica was required and none is eligible: unhealthy, behind the session, or none configured.
OKM1844Fallback refusedAn automatic read found no eligible replica and fallback is error.
OKM1845Target unresolvedThe target could not be resolved. The tenant is unknown or the resolver failed.
OKM1846Acquire timeoutThe pool did not hand out a connection within timeouts.acquire.
OKM1850Protected targetThe operation is blocked on a protected target and --allow-protected was not passed.
OKM1851Target not emptyProvisioning was asked to fill a target that already has objects.
OKM1852Protection mismatchTargets that resolve to the same database disagree about protection, or tenants of one schema-per-tenant database do.
OKM1853Target not namedSeveral targets are configured and the command was not given --target.
OKM1854Pooler sessionTwo statements on the same connection saw different backend pids. A transaction-mode pooler is in the way.
OKM1855Pull would overwriteokm pull or okm import drizzle found a file it would replace. It writes nothing.
OKM1856Drizzle is not installedokm import drizzle could not resolve drizzle-orm from the project. Install it in that project.