# Documentation (/docs)

The schema is the source. Migrations and queries come from it. Start with one table, then add only the part you need.

0.9 adds tooling around that schema: [pull](/docs/reference/cli#pull) writes table files from a database, [import drizzle](/docs/start/from-drizzle) writes them from a Drizzle schema, [catalogs](/docs/define/schema/catalogs) share one database, [describe](/docs/reference/schema-api) returns the catalog as JSON, [extension packs](/docs/define/schema/extensions) cover the contrib set except `vector`, the [GitHub Action](/docs/reference/github-action) posts the plan, and a [type change](/docs/reference/type-change) that rewrites a table is five migrations.

<Callout title="The source">
  The tables you declare are the catalog. Queries, migrations, and checks come from that catalog.
</Callout>

```ts title="schema.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});
```

## 01 Start

Add the package, then one table. The next page is one example per call.

<Cards>
  <Card
    title="Let's start"
    description="Install a driver, define the table, then push and connect."
    href="/docs/start"
  />
  <Card
    title="Basic Usage"
    description="Each call on the client: insert, find, one, count, exists, update, and delete."
    href="/docs/start/basic-usage"
  />
</Cards>

## 02 Define

Each table, the schema, the client, and the config file. Open the page for the object you are setting.

<Cards>
  <Card
    title="Table"
    description="table() stores the columns. Open it for the key, each data type, and the queries."
    href="/docs/define/table"
  />
  <Card
    title="Schema"
    description="schema() compiles the tables into one catalog."
    href="/docs/define/schema"
  />
  <Card
    title="Connect"
    description="connect() opens the client. The first argument is the target."
    href="/docs/define/connect"
  />
  <Card
    title="Config"
    description="The file okm reads. Targets, paths, timeouts, and roles."
    href="/docs/define/config"
  />
</Cards>


# LLM (/docs/ai/llm)

[llms.txt](/llms.txt) lists every handbook page. Fetch that index, then one page.

The site generates the file from the handbook. The repository does not store a copy at the root.

<Callout type="info" title="Start here">
Fetch `/llms.txt`. Open one linked page. Use `/llms-full.txt` when you need the whole handbook.
</Callout>

## Discovery

Every response sends a `Link` header:

| Target | Relation |
| --- | --- |
| `/llms.txt` | `describedby` |
| `/sitemap.xml` | `sitemap` |
| `/.well-known/agent-skills/index.json` | `agent-skills` |

A handbook URL also sends `rel="alternate"` for its markdown twin. The same three relations are `<link>` elements in the page head. `/robots.txt` sets `Content-Signal: search=yes, ai-input=yes, ai-train=yes` on every crawler group.

## The index

`/llms.txt` is `text/markdown`. The public origin is `https://okm.omqkhafi.dev`. Set `SITE_ORIGIN` when the site is served elsewhere.

The body is the site name, the package description, and one instruction: fetch `/llms.mdx/docs/⟨slug⟩.md` for one page, or [/llms-full.txt](/llms-full.txt) for every page. Root pages sit under `Start`. Each sidebar folder is its own heading, in sidebar order. A page line is the title, the absolute markdown URL, and the page description.

`Also` links the [changelog](/changelog), `llms-full.txt`, the GitHub repository, and the npm package.

The [sitemap](/sitemap.xml) lists `/llms.txt`, `/llms.json`, and `/llms-full.txt`. `robots.txt` allows every crawler, including the named AI user agents, and points them at that sitemap.

## One page

A handbook page as markdown is `/llms.mdx/docs/⟨slug⟩.md`. The introduction is `/llms.mdx/docs/index.md`.

The body is the source file with the YAML frontmatter removed. The title is the heading.

Send `Accept: text/markdown` on the HTML URL. The site rewrites `/docs/⟨slug⟩` to that file, `/` to `/llms.mdx/home`, and `/changelog` to `/llms.mdx/releases` (a series is `/llms.mdx/releases/⟨series⟩`). Those responses set `Vary: Accept`.

When `Accept` prefers markdown and no page matches, the response is a markdown 404. The body links the handbook, `llms.txt`, and the sitemap.

## The full set

[/llms-full.txt](/llms-full.txt) joins every handbook page. Each block opens with the title and the HTML path.

[/llms.json](/llms.json) is the same set as JSON: name, description, origin, the index URL, the full-text URL, and one object per page with `slug`, `title`, `description`, `html`, and `markdown`.

## In the repository

Read these when changing OKModel. The handbook is the public guide. Normative text lives in `docs/`.

| File | What it is |
| --- | --- |
| `AGENTS.md` | Repository rules. Root `CLAUDE.md` and `site/CLAUDE.md` import it. Toolchain, layers, scripts, and the size and type ceilings. |
| `docs/` | Normative text. The spec is `docs/okmodel-api-design.md` (draft 26), implemented through the 0.9.1 release. Decisions are `docs/okmodel-decisions.md` (D1–D296). |
| `site/content/docs/` | Handbook source. This is what `llms.txt` indexes. |
| `.agents/skills/okm-ship/SKILL.md` | Close user-visible work: notes under `## Unreleased`, then `bun run bump next`. |
| `.cursor/rules/` | Cursor rules for this repo, including the project map. |
| `site/AGENTS.md` | Next.js rules for the docs app. |

The index route is `site/app/llms.txt/route.ts`. The full text, the JSON catalogue, and the per-page routes sit beside it under `site/app/`.

Read `AGENTS.md` before an edit. Read the spec and the decision when the change is behavior. Fetch one handbook page when the question is how a caller uses the library.


# MCP (/docs/ai/mcp)

okmodel does not ship an MCP server. There is no package to add, no config file, and no tools to call.

An MCP server is deferred. If one is built, it is a separate package outside the core. It is not scheduled.

## In the browser

The site registers three read-only tools when the browser has `document.modelContext` or `navigator.modelContext`. A browser without that API does not download the script.

| Tool | Input | Reads |
| --- | --- | --- |
| `search_docs` | `query` | `/api/search` |
| `get_page` | `slug`, such as `reference/cli` | That page as markdown |
| `list_pages` | none | `/llms.json` |

## The handbook

An agent reads the site. Start at [`/llms.txt`](/llms.txt). The index lists every public page as markdown, in sidebar order. Each line is the page title, its markdown URL, and the page description.

| URL | Returns |
| --- | --- |
| `/llms.txt` | The index, as markdown. |
| `/llms.mdx/docs/⟨slug⟩.md` | One page. The title is the heading. YAML frontmatter is stripped. |
| `/llms-full.txt` | Every page in one file. Each block starts with the title and the HTML path. |
| `/llms.json` | The same pages as JSON. Each page has `slug`, `title`, `description`, `html`, and `markdown`. |

The introduction is `/llms.mdx/docs/index.md`. A nested page keeps its path, such as `/llms.mdx/docs/define/table/columns.md`. The same route also accepts a trailing `content.md` and the slug without `.md`.

Send `Accept: text/markdown` on a handbook URL and the response is that page's markdown. The response sets `Vary: Accept`. This covers `/`, `/docs`, and `/changelog`. A URL under `/llms` is already markdown and is served as written. An unknown path with that `Accept` header is a markdown 404. The body links `/docs`, `/llms.txt`, and `/sitemap.xml`.

## Errors

`OkmError`, from `okmodel`, carries `fix` for a person or a coding agent. `fix.summary` is one sentence. `fix.suggestion` is the nearest accepted name, when one is close.


# Skills (/docs/ai/skills)

The agent skills in this repository, and when to run each one.

A skill is a file an agent reads and follows. Skills live at `.agents/skills/<name>/SKILL.md`. The `description` in that file says when it applies. This repository ships one skill.

The site publishes one skill for agents that are not in this repository. The index is [`/.well-known/agent-skills/index.json`](/.well-known/agent-skills/index.json). The file is [`/.well-known/agent-skills/okmodel/SKILL.md`](/.well-known/agent-skills/okmodel/SKILL.md). It says to start at `/llms.txt`, then fetch one page. The index pins that file with a SHA-256 digest.

## okm-ship

`.agents/skills/okm-ship/SKILL.md` closes an implementation. Run it before you say the work is done. A missing changelog note for user-visible work is a defect.

`AGENTS.md` and `.cursor/rules/changelog.mdc` send the agent to this file. Follow the skill file.

### When it applies

After a change to behavior, the public API, the CLI, a dialect, an adapter, errors, or the normative docs. Also when the prompt says to ship, close out, or sync the changelog.

A pure typo or a format-only edit stays out of the changelog and skips the bump.

### What it requires

Three steps, in order. Finish them before the work is called done, and before the commit.

| Step | Action |
| --- | --- |
| 1 | From the diff, list what a user or an app author would notice. |
| 2 | Append those notes to `changelog.md` under `## Unreleased`. |
| 3 | Run `bun run bump next`. |

A note that would describe an API the source does not have stops the skill. Ask before writing it.

Create a group only when it has bullets. Keep this order:

| Group | Use it for |
| --- | --- |
| `### ✨ Added` | A new capability, export, or command. |
| `### 💥 Breaking Changes` | An incompatible API, config, or behavior. |
| `### ♻️ Changed` | A behavior or default change. |
| `### ⚠️ Deprecated` | A deprecation. |
| `### 🔥 Removed` | A removal. |
| `### 🐛 Fixed` | A bug fix. |
| `### 🔒 Security` | A security-relevant change. |

A group with 8 or more bullets gets `####` areas, and only for areas that have bullets. Order: `contracts`, `dialects`, `adapters`, `runtime`, `tooling`, `docs`. The area is where the change lives. The CLI, scripts, checks, and CI are `tooling`. Files under `docs/` are `docs`. A smaller group stays flat.

One idea per bullet. Write the user-visible impact. Put the bullet in its group, under its area when the group is large.

| Prompt | Notes go under | Command |
| --- | --- | --- |
| An implementation | `## Unreleased` | `bun run bump next` |
| The release prompt of a train | `## Unreleased`; the command promotes that section | `bun run bump release` |
| An untagged release the prompt names, such as `v0.6.0` | That existing `## vX.Y.Z` | Leave `package.json` as it is |

`bun run bump next` sets `package.json` to `<next release>-next.N` and leaves the changelog in place. `bun run bump release` drops the `-next.N` suffix, renames `## Unreleased` to `## vX.Y.Z — <date>`, and leaves a fresh empty `## Unreleased`.

Hygiene finishes before a release bump. A hygiene prompt that is not itself the release still runs `bun run bump next`. The release prompt of a train is P17B for 0.1.0, then P30, P44, P55, P66, P86, P99, P110, and P119. That prompt runs `bun run bump release`.

`patch`, `minor`, `major`, and `--set` remain available. Each of those still promotes the changelog. `--dry-run` prints the plan and writes nothing.

<Callout type="info" title="Untagged release">
When the prompt names a release that has no tag yet, the notes go under that heading. `bun run bump` stays unrun. `bun run release-check` allows this until the tag exists.
</Callout>

Before the execution report, review the change against the engineering standards in the skill (D129). The report states runtime entry size, minified and gzip, cold import, and any type-cost change. It also states the app startup graph, the total graph, and connect-entry cold imports. A measurement within 3% of a type ceiling is called out. A path listed in `.cursor/rules/project-map.mdc` that moved is updated in that same change.

### What it must not do

- Append a bullet under a tagged `## vX.Y.Z`. Current notes go under `## Unreleased`.
- Write a `## v…` heading. `bun run bump release` writes it.
- Add a group or an area outside the lists above.
- Describe an API the source does not have. Stop and ask.
- Run `bun run bump release` outside the release prompt of a train.
- Run `bun run bump` when the prompt names an untagged release.


# CLI (/docs/reference/cli)

`okm` reads `okmodel.config.ts` from the directory it starts in. The `okmodel` bin is the same program. The file default-exports `defineConfig` from `okmodel/migrate`. Each option is a page under [Config](/docs/define/config).

`okm push` is for a database you can recreate. A shared database uses reviewed migrations: `okm generate init`, then `okm migrate apply`.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
});
```

## Run

Node.js 22.18 or newer loads the config. `bunx okm` uses Node. `bun --bun okm` uses Bun.

<Callout type="info" title="Node.js">
A process that cannot load a `.ts` config exits 1. The line is `okm needs Node.js >=22.18 to load a .ts config (type stripping). This process is <version>. Run bun --bun okm to use the Bun runtime.`
</Callout>

| Invocation | Result |
| --- | --- |
| No arguments, `help`, `--help`, or `-h` | Prints `okm <command>` and one line per command. |
| `okm <words> --help` | Prints the first command whose name is those words, or starts with them. `okm migrate --help` prints `migrate plan <name>`. An unknown name prints the list. |
| `--version` | Prints the package version and does not run a command. The Node.js check still runs first. |
| A failure | Exit code 1. Stderr is `error <code>: <message>` and a fix line. |
| `--verbose`, or `OKM_DEBUG=1` | That failure also prints the stack. |

An unknown command is `Unknown command <name>.` `okm migrate` with a subcommand other than `plan`, `apply`, `status`, or `check` is `Unknown command migrate.`

## Commands

| Command | Result |
| --- | --- |
| `build` | Writes the catalog under [out](/docs/define/config/out). |
| `catalog export` | Writes the catalog snapshot a dependent verifies. |
| `check` | Lints the schema. Compares one database when a target is selected. |
| `dev` | Names a target called `dev`, or creates a PGlite database and prints its directory. |
| `doctor [code]` | Lists triggers, checks [roles](/docs/define/config/roles) when they are set, or prints one error code. |
| `driver test <driver>` | Runs the conformance suites against one driver. |
| `ext list` | Lists extensions the server can install. |
| `ext check` | Compares declared extensions with the server. |
| `ext scaffold <name>` | Prints a starting `extension()` definition, including composite types. `--out` writes it and never overwrites. |
| `ext test <file>` | Checks that definition against the server. |
| `generate [name]` | Writes the next migration. |
| `import drizzle <path>` | Converts a Drizzle schema into table files. Runs that file. Needs no database. |
| `migrate plan <name>` | Prints the plan. Adds lock estimates when one target answers. `--json` prints a document. |
| `migrate apply` | Applies pending migrations. |
| `migrate status` | Prints each target's version and state. |
| `migrate check` | Replays migration history in a scratch schema and rolls it back. |
| `push` | Applies the schema diff. Writes no SQL file. |
| `pull` | Reads one database and writes one table file per table, plus the schema file. |
| `seed <file>` | Runs the file's default export on one target. |

## Flags

`check`, `generate`, `push`, `pull`, `migrate`, `seed`, `doctor`, and `ext` parse the flags in this table. `build` and `dev` leave the rest of the line unread. `driver test` reads `--url` only, in [driver test](#driver-test). `pull` reads `--target`, `--schema`, and `--out` itself.

| Flag | Read by |
| --- | --- |
| `--target <name>`, `--target=<name>` | `check`, `migrate plan`, `migrate apply`, `migrate status`, `migrate check`, `push`, `pull`, `seed`, `ext`, and `doctor` when [roles](/docs/define/config/roles) is set. Who requires it is [Database](/docs/define/config/database). |
| `--allow-protected` | `migrate apply`, `push`, and `seed`. One invocation. Blocked work is [Protection](/docs/define/config/database#protection). `migrate check` ignores the flag. |
| `--allow-pooler` | `migrate apply`, `push`, `seed`, `migrate check`, and `check` when it compares a database. Same opt-in as [allowPooler](/docs/define/config/pooler). Either one is enough. |
| `--lock-timeout <milliseconds>` | `migrate apply`. Overrides [timeouts.lock](/docs/define/config/timeouts). `0` disables it. |
| `--statement-timeout <milliseconds>` | `migrate apply`. Overrides [timeouts.statement](/docs/define/config/timeouts). |
| `--json` | `migrate plan`. Prints a JSON document instead of the SQL text. The lock strings match the human plan, including row estimates. |
| `--replace <table>.<column>.<old>=<new>` | `generate` and `migrate plan`. Repeat it for each removed value. `=null` is the null replacement, on a nullable column. |
| `--provision` | `migrate check`. Compares a snapshot install with a full replay. |
| `--schema <name>`, `--schema=<name>` | `pull`. Repeat it, or separate names with a comma. The default is the target's current schema. |
| `--out <directory>`, `--out=<directory>` | `pull` and `import drizzle`. Writes `schema.ts` and `tables/` there. Without it, the command uses the schema path and the tables directory from the config, or `schema.ts` and `tables/` when `import drizzle` has no config yet. |

A missing `--target` name is OKM1853: `--target needs a name.` A missing `--replace` value is OKM1541: `--replace needs table.column.old=new.` `--lock-timeout` and `--statement-timeout` need a finite number of milliseconds that is not negative. The message names the flag. `check`, `push`, and `migrate check` refuse a bad number and do not apply it. The same parser runs on the other commands in the list above.

## build

`okm build` validates the schema and writes the [out](/docs/define/config/out) directory. The default is `.okm`. It prints that directory. It does not connect.

The files are `catalog.json`, `catalog.hash`, and `types.d.ts`. `catalog.hash` is the SHA-256 of `catalog.json`. `types.d.ts` is the row types. The mode header is on [Types](/docs/define/schema/types).

```sh
bunx okm build
```

## pull

`okm pull` reads one database and writes one table file per table, plus the schema file. It does not write the selected namespaces, so a protected target is allowed. It creates a scratch schema to verify functions and views, then drops it. An existing file is OKM1855 and nothing is written. `--out <directory>` is the other place to write. The default namespace is the target's current schema. `--schema <name>` selects others and may be repeated.

Export bindings and file stems are unique when folded to lowercase, so `Tasks` and `tasks` become `Tasks` / `tasks_2` and do not collide on a case-insensitive volume or in row types. A Windows-reserved stem (`con`, `aux`, `nul`, `com1`–`com9`, `lpt1`–`lpt9`, any case) gets a `_` prefix. The SQL name stays in `sqlName` when the binding differs. A column named `__proto__` is written as a computed key. Single-column foreign keys name the exported table binding, not the raw SQL table name. When `oxfmt` is missing, the files stay unformatted, a notice is printed, and the report still returns.

Tables, columns, keys, uniques, checks, single-column foreign keys, indexes, identity sequences, enums, domains, extensions, functions, triggers, views, and materialized views are written. A view is SQL text with its columns. Views and `BEGIN ATOMIC` bodies take dependencies from `pg_depend`. A plpgsql or SQL-string body gets `dependsOn` candidates from a lexical match. The report marks each function `verified` (the scratch replay created it with `check_function_bodies` on), `candidate-only` (plpgsql: creation does not prove the body), or `unverified` (creation failed). A comment above the declaration says the same thing. Policies, roles, grants, aggregates, procedures, event triggers, and rules are printed under `not pulled yet` with a count, and so is anything `table()` cannot say. A column type with no builder is that list, with the table and the column. Nothing is dropped without a line.

```sh
bunx okm pull
bunx okm pull --schema public --out ./adopted
```

## import drizzle

`okm import drizzle <path>` reads a Drizzle schema and writes one table file per table, plus the schema file. It imports that path, so the file runs. The `drizzle-orm` package is the one installed in the project. okmodel does not depend on it. A missing package is OKM1856: install `drizzle-orm` in the project. An existing file is OKM1855 and nothing is written. The command does not connect.

The report starts with `namespace <name>`. When the name is not `public`, set [namespace](/docs/define/config/namespace) to it. `schema()` still compiles `public`. File stems follow the same lowercase-folded uniqueness and Windows-reserved rules as [pull](#pull). Composite primary-key columns are not null even when Drizzle omitted `.notNull()` on the column.

Composite foreign keys, `relations()`, and custom column types are printed under `not imported` with the table and the reason. A `serial` column is written as `integer` not null. The sequence `serial` would create is listed, not written.

```sh
bunx okm import drizzle ./src/db/schema.ts --out ./adopted
```

## catalog export

`okm catalog export` writes the column snapshot of the schema this project owns: table names plus each column's name, type, and nullability. It is not the full catalog; that is [`describe(schema)`](/docs/reference/schema-api). It does not connect. `--json` prints the JSON. `--out <file>` writes that file and prints the path. With neither, the JSON is printed. `--json` and `--out` together print the JSON and write the file.

The file has `catalogHash` and `logical` (`namespace`, `name`, `format`). A table is a namespace, a name, and columns. There is no function and no connection URL. A dependent lists the path in [`external`](/docs/define/config/external).

```sh
okm catalog export --out identity.okm.json
```

## check

`okm check` lints the schema against the previous snapshot, then prints warning findings and `ok`. An error finding is OKM1510 and the command exits 1. A finding line is `<severity> <code> <place>: <reason> -- fix: <fix>`. An external table is checked against each [`external`](/docs/define/config/external) snapshot and, when a target is selected, against that database. A stale or missing snapshot is OKM1841. A foreign key whose namespaces differ is a `coupling` warning. A foreign key into another database is OKM1842.

A [tables](/docs/define/config/tables) directory is scanned for a `.ts` file whose name is absent from the schema module. That is OKM1024: `Table file not in schema: <file>.` A schema that would validate on write, with no runtime import of `okmodel/validate`, is OKM1201. The rules are on [Validation](/docs/define/schema/validation).

With one configured target, or with `--target`, a database that already has `okm_meta` is compared with the schema. A difference is OKM1520: `The database differs from the schema.` Several targets and no `--target` skip that comparison. No target skips it too. An unknown `--target` is still OKM1845. A protection mismatch on the same database is OKM1852. Both cases are on [Database](/docs/define/config/database).

## dev

`okm dev` looks for a target named `dev`. It prints `target dev` when that target is configured. With no such target it creates a PGlite database at `<out>/dev-db`, runs `select 1`, closes it, and prints that directory. The default `out` is `.okm`. The command then returns. It does not watch the schema, does not reload, and does not apply migrations.

```sh
bunx okm dev
```

## driver test

`okm driver test <driver>` runs the conformance suites against one driver and prints a report. `<driver>` is `pglite`, `postgresjs`, `pg`, `bun`, `neon`, or a path to a module. The module exports `name`, `capabilities`, `open`, and `connect`. `memory: true` means one in-process connection. `notices: false` means the driver does not surface `RAISE NOTICE`. `size` is the pool size the stats case expects. Omitted, `memory` is false, `notices` is true, and `size` is 1 for an in-process driver and 4 otherwise. Bun must be `>=1.4.0` (`package.json` `engines.bun`); a lower Bun stops with a clear message. Each wire and error case drops its `okm_*` tables before the pool closes.

`--url <postgres-url>` is the database. `--url=<postgres-url>` is the same. `DATABASE_URL` is used when the flag is omitted. `pglite` does not connect to it. The other built-in drivers need one of the two. The command does not read `okmodel.config.ts`.

The report starts with `driver <name>` and `dialect postgres`. Then one row per case, tab-separated: case id, behavior, result, capability. The result is `pass`, `fail`, or `unsupported`. A case that does not run is `unsupported` (or `fail`), never `pass`. The capability is set on an unsupported row and names the limit (`cancel`, `describe`, `stream`, `listen`, `notices`, `one connection`, `no socket to cut`). A failure exits 1. An unsupported case does not.

PGlite has no socket to cut, so COMMIT loss is `unsupported` with capability `no socket to cut`. Neon uses the same row: the harness cut is a TCP relay, and Neon speaks WebSocket or HTTP.

```sh
okm driver test pglite
okm driver test postgresjs --url "$DATABASE_URL"
```

## doctor

`okm doctor <code>` prints one registry entry and does not load the project.

```text
OKM1520: Schema drift
The database has drifted past what an expand migration can repair.
Inspect the diff and repair the database, or generate a migration that matches the drift.
```

An unknown code is `Unknown code <code>.`

With no code, the command lists each table's triggers, one line `<table>: <name>, <name>`, in table order. No trigger is `no triggers`. When [roles](/docs/define/config/roles) is set, it also connects and checks external roles, `CREATEROLE` when a role is managed, and the application role's grants. A missing managed role is left for the plan. That connection is the read-only `check` class. A protected target is allowed. The command does not change the database.

```sh
bunx okm doctor OKM1520
```

## ext

`okm ext list` and `okm ext check` connect to the selected target as the read-only `check` class. A protected target is allowed. A pooler host is not refused.

`list` prints a tab-separated table. The header is `name`, `default`, `installed`, `schema`.

`check` compares extensions declared on the schema with what is installed. No declaration is `no extensions`. A missing install is OKM1811: `Extension <name> is not installed on the server.` A version the declaration rejects is OKM1812: `Extension <name> needs <declared> or higher. The server has <installed>. Install a version the declaration accepts, or change the pin.` An exact pin omits "or higher". A match prints `name`, `declared`, `installed`. An omitted pin is `-`. Declarations are on [Extensions](/docs/define/schema/extensions).

`scaffold <name>` reads `pg_depend`, `pg_type`, `pg_proc`, `pg_operator`, and `pg_settings` for an extension that is already installed. A missing install is OKM1811. The text is a starting `extension()` definition the project owns. `--out <file>` writes that text. An existing file is `invalid` and nothing is written. Without `--out` the text is printed.

`test <file>` loads that module and checks each exported `extension()`. Every declared type, operator, function, and index method must exist with that signature. A mismatch is `invalid` and the message lists each one. A `requires` version or Postgres floor the server does not meet is OKM1812. A match prints `ok <name>`.

Another name is `Unknown command ext <name>.`

```sh
bunx okm ext check
```

## generate

`okm generate` plans the schema against the last `*.catalog.json` in name order and writes SQL. The name defaults to `migration`. Letters, digits, `_`, and `-` are accepted. Anything else is `Migration name <name> must be letters, digits, _ or -.`

The file is `<number>_<name>.sql` in the [migrations](/docs/define/config/migrations) directory, with `<number>_<name>.catalog.json` beside it. The number is one higher than the highest numeric prefix, at least four digits. An empty directory starts at `0001`. The name `init` also writes `.gitattributes` in the project when that file is missing.

An empty plan writes no SQL file and prints `no changes`. The [out](/docs/define/config/out) catalog is still written. A written file prints its path, then one line per lint finding. An error finding does not fail this command. `migrate plan` and `check` exit 1 on the same finding.

`--replace` records a removed picklist or enum value. The schema stores no replacement. A chain, a value replaced by another removed value, is OKM1541. `=null` on a column that is not nullable is OKM1541.

```sh
bunx okm generate init
```

```sh
bunx okm generate rename-status --replace notes.status.draft=open
```

## migrate plan

`okm migrate plan <name>` prints the plan and writes nothing. A missing name is `okm migrate plan needs a name.`

With one target, or with `--target`, a database that answers adds `pg_class` row estimates to the lock lines. No target, several targets and no `--target`, or a target that cannot be reached, prints the plan without estimates. An unknown `--target` and a protection mismatch still throw. Those are the same refusals as [Database](/docs/define/config/database). A pooler host is not refused.

An error finding is OKM1510 and the command exits 1 after the plan text. `--json` prints one document and no SQL text: `name`, `class`, each step's `class`, `lock`, and `sql`, and the findings (`severity`, `code`, `place`, `reason`, `fix`). `lock` is the human `-- lock:` line. The command still exits 1 after that document when a finding is an error. `--replace` is the same flag as [generate](#generate).

```sh
bunx okm migrate plan init
```

## migrate apply

`okm migrate apply` applies pending files in the [migrations](/docs/define/config/migrations) directory. A file without its `.catalog.json` is refused. With no migration files, or no head snapshot, it prints `target <name>` and `nothing to apply` and does not connect.

A pending file is linted after `okm_history` is read and before any DDL. `lintFrom` is not read. An error finding is OKM1510. Transaction control is the finding OKM1551. Apply accepts that step when it carries `-- okm-allow OKM1551: reason`. An empty reason, or a code the statement did not trigger, is OKM1510. [migrate check](#migrate-check) still refuses the step.

The summary is `target <name>`, then `set role <role>` when apply issued `SET ROLE`, then `restamped <id>` for a history repair, then `applied <id>` for each file. Nothing applied is `nothing to apply`. A backfill batch is printed as it commits:

```text
backfill <migration> step <index> batch <n> rows <n> key <last>
```

`key` is the committed boundary, or `-` when that batch has none.

A protected target allows expand, reference rows, and provision of an empty database. Contract, unclassified SQL, a backfill, and a history repair that changes a row are OKM1850 until `--allow-protected`. An empty database, a database that already has objects and no history, and a provision that stopped part-way are on [Database](/docs/define/config/database#an-empty-database). Reference rows are on [Reference rows](/docs/define/table/reference-rows). Timeouts are on [Timeouts](/docs/define/config/timeouts). Batch size, pause, and the batch statement timeout are on [Backfill](/docs/define/config/backfill). Roles are on [Roles](/docs/define/config/roles).

A schema module that does not export a `schema()` still lets apply run. It installs no reference rows. The message is on [Schema](/docs/define/config/schema).

```sh
bunx okm migrate apply --target prod
```

## migrate status

`okm migrate status` prints every configured target, in name order. `--target` keeps one name. Several targets do not need `--target`. No target is OKM1845.

The columns are tab-separated: `target`, `version`, `catalog`, `state`, `protected`. `protected` is `true` or `false`. The state is `current`, `behind by expand`, `behind by contract`, `ahead by expand`, `ahead by contract`, or `failed at step <n> (resume with okm migrate apply)`.

An unfinished backfill follows as its own section: a line `backfill`, then the tab-separated header `migration`, `step`, `rows`, `key`, `state`.

A pooler host is not refused. The command is read-only, including on a protected target.

```sh
bunx okm migrate status
```

## migrate check

`okm migrate check` replays every migration into a scratch schema inside one transaction, then rolls that transaction back and drops the schema. The schema name starts with `okm_check_`. A passing run prints `ok <n> migrations`. A statement left unrun follows on its own line, `<id> step <n>: <reason>`. The reasons are `VACUUM is not run inside the check transaction`, `CREATE or DROP DATABASE is not run inside the check transaction`, and `ALTER SYSTEM is not run inside the check transaction`.

A protected target is OKM1850. The message is `okm migrate check writes a scratch schema and refuses protected target <name>. Point it at a throwaway Postgres.` `--allow-protected` does not lift it.

The command also checks the previous catalog, the head, and the linter. The first linted id is [lintFrom](/docs/define/config/lint-from). An error finding is OKM1510. Transaction control (`begin`, `commit`, `rollback`, `abort`, `end`, `start transaction`, `prepare transaction`), including inside a `DO` body, is that finding, OKM1551. A function or procedure body is not this. When the finding is allowed, the replay still throws OKM1551: `Migration <id> step <n> contains <word>.`

`--provision` also installs the head snapshot and compares it with a full replay. A difference is OKM1521: `Provisioning from the snapshot does not match the replayed history.`

[timeouts](/docs/define/config/timeouts) apply when the config sets them. The flags do not. A pooler host is refused until [allowPooler](/docs/define/config/pooler) or `--allow-pooler`.

```sh
bunx okm migrate check --provision
```

## push

`okm push` plans the schema against the live catalog on the target and applies that plan as one migration named `push-<n>`, where `n` comes from `okm_history`. It writes no SQL file. An empty plan that already matches `okm_meta` prints `target <name>` and `no changes`. When the hash moved with no DDL, it re-stamps `okm_meta` and prints `restamped push-<n>` then `nothing to apply`. A plan with steps uses the apply summary.

A protected target is OKM1850 until `--allow-protected`. Lock and statement timeouts stay 5000 and 30000 milliseconds. The config and the flags are not applied. A pooler host is refused until [allowPooler](/docs/define/config/pooler) or `--allow-pooler`.

```sh
bunx okm push
```

## seed

`okm seed <file>` loads that module and calls its default export with the testing harness for the selected target. The path is resolved from the project directory. A missing file is `Seed file <file> was not found.` A default export that is not a function is `<file> must default-export a function.` A missing file argument is `okm seed needs a file.`

The target is already migrated. The harness does not apply the catalog. A protected target is OKM1850 until `--allow-protected`. Several targets need `--target`. A pooler host is refused until [allowPooler](/docs/define/config/pooler) or `--allow-pooler`.

The command prints `target <name>` and one line `<table> <count>` for each table the factories inserted, in table name order. No insert is `created nothing`.

Under `rlsTenancy` the command runs as the application role. The owner, a superuser, or a `BYPASSRLS` role is OKM1707.

```ts title="seed.ts"
import type { Testing } from "okmodel/testing";

export default async function seed(t: Testing): Promise<void> {
  const f = t.factories({
    notes: (x) => ({ title: x.words(2) }),
  });
  await f.notes.create();
}
```

```sh
bunx okm seed seed.ts
```


# Environment Variables (/docs/reference/environment-variables)

`connect` and `defineConfig` do not read the environment. `okm` reads `OKM_DEBUG`. Local Postgres reads `POSTGRES_VERSION`, `REQUIRE_DOCKER`, and the three host ports.

## Client

The client takes the URL you pass. [Target](/docs/define/connect/target) is that argument. [Database](/docs/define/config/database) is the field in `okmodel.config.ts`.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

const url = process.env.DATABASE_URL;
if (url === undefined || url.length === 0) throw new Error("DATABASE_URL is not set");

export const db = connect(url, { schema: app });
```

`DATABASE_URL` is the name in the handbook. `connect` does not look it up. A missing or empty value is passed through. Check it before the call.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
});
```

A missing or empty `database` is OKM1845. `Target default has no url.` The config loader does not fill it from the environment.

| Name | Result |
| --- | --- |
| `NODE_ENV` | Not read. It does not select a target, set `protected`, or turn on [`requireMeta`](/docs/define/connect/check). |
| `OKM_ALLOW_PROTECTED` | Not read. `--allow-protected` is that invocation. Setting the variable leaves OKM1850 in place. |

A replica URL is the same kind of argument. [Topology](/docs/define/connect/topology) takes the strings you pass.

<Callout type="info" title="The URL">
`connect` and `defineConfig` receive the string you pass. They do not read `DATABASE_URL`.
</Callout>

## CLI

`okm` reads one variable, and only when a command throws.

| Variable | Read by | Accepted | Default | Unset |
| --- | --- | --- | --- | --- |
| `OKM_DEBUG` | the `okm` bin | `1` | none | `error <code>: <message>`, then the fix. No stack. |

`1` appends the stack after those two lines. Any other string leaves the stack off. `--verbose` prints the stack for that invocation.

```sh
OKM_DEBUG=1 okm check
```

Migrate commands read the config and the flags. The target, protection, and the apply timeouts are those fields. See [Database](/docs/define/config/database) and [Timeouts](/docs/define/config/timeouts).

## Tests and local Postgres

`bun run db:up`, `bun run db:down`, `bun run test:postgres`, and `bun run verify` read these. The published client does not. PGlite has no server and does not read them.

### Version

| Variable | Read by | Accepted | Default | Unset |
| --- | --- | --- | --- | --- |
| `POSTGRES_VERSION` | `db:up`, `db:down`, `verify`, `test:postgres` | `15`, `16`, `17`, `18` | `17` | `17`. The image is `postgres:17`. |

Any other string, including `""`, throws `Postgres version '<value>' is outside 15–18.` `bun run db:up` and `bun run db:down` print that after `[db]` and exit 1.

`bun run verify` uses this major. `bun run verify --all` runs 15, 16, 17, and 18, and sets `POSTGRES_VERSION` to the major it is starting. `bun run test:postgres` uses the major for the suite plan. It does not start Docker.

### Docker

| Variable | Read by | Accepted | Default | Unset |
| --- | --- | --- | --- | --- |
| `REQUIRE_DOCKER` | Postgres tests | `1` | none | Skip when Docker or the topology is down. |

Only `1` fails the test. `true` and `0` skip.

| Probe | `1` | Unset |
| --- | --- | --- |
| Docker is not running | REQUIRE_DOCKER=1 but Docker is not running. | Skipping Postgres tests: Docker is not running. |
| Docker is running, Postgres is closed | REQUIRE_DOCKER=1 but Postgres is not accepting connections. Run `bun run db:up`. | Skipping Postgres tests: Docker is running but Postgres is not up. Run `bun run db:up`. |
| The primary accepts connections | The test runs. | The test runs. |

`bun run test:postgres` and `bun run verify` set `REQUIRE_DOCKER=1` for the test process. The probe opens the topology primary. A `DATABASE_URL` in the environment does not move it.

### Ports

The primary URL is `postgres://okm:okm@127.0.0.1:<port>/okm`. Replica `a` and replica `b` use the same user, password, host, and database.

| Variable | Read by | Accepted | Default | Unset |
| --- | --- | --- | --- | --- |
| `OKM_PRIMARY_PORT` | `db:up` and the primary URL | a host port | `55432` | `55432` when that port is free |
| `OKM_REPLICA_A_PORT` | `db:up` and the replica `a` URL | a host port | `55433` | `55433` when that port is free |
| `OKM_REPLICA_B_PORT` | `db:up` and the replica `b` URL | a host port | `55434` | `55434` when that port is free |

When all three are unset and one of `55432`–`55434` is taken, `db:up` binds three free ports and prints `[db] Postgres <version> topology is up (primary <port>, replicas <a> and <b>).` Tests still open `55432`–`55434` until you set the variables to the printed ports.

Setting any one of the three pins all three. An omitted name stays at its default, and the free-port choice does not run.

When `GITHUB_ENV` is set, `db:up` appends the three ports for the next step. Locally that file is unset, and the shell that ran `db:up` is unchanged.

```sh
POSTGRES_VERSION=16 bun run db:up
REQUIRE_DOCKER=1 bun test
```


# Errors (/docs/reference/errors)

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

```ts title="catch.ts"
import { OkmError } from "okmodel";

export function respond(error: unknown) {
  const err = OkmError.from(error); // [!code focus]
  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

| Field | What it holds |
| --- | --- |
| `code` | `OKM1xxx`, or the kind when that kind has no number. |
| `kind` | One of the kinds below. |
| `category` | The category of that kind. |
| `summary` | What failed. Same string as `message`. |
| `retryable` | Whether a declared retry may run the call again. |
| `fix` | `{ summary, suggestion? }`. `suggestion` is a nearer accepted name. |
| `table` | Table the database named, when it named one. |
| `columns` | Columns the failure names. Empty when it names none. |
| `constraint` | Constraint name the database sent. |
| `sqlstate` | SQLSTATE the database sent. |
| `batchIndex` | Statement index inside a batch, or `null` on one statement and at commit. |
| `issues` | Validation 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](/docs/define/connect/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.

```ts title="match.ts"
import { OkmError } from "okmodel";

export function formOrHttp(error: OkmError) {
  return error.match({
    input: (item) => item.fields(), // [!code focus]
    conflict: (item) => item.fields(), // [!code focus]
    _: (item) => item.toHttp(), // [!code focus]
  });
}
```

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

```ts title="unique.ts"
import { OkmError } from "okmodel";

export function emailTaken(error: unknown): boolean {
  return OkmError.is(error, "unique", "notes"); // [!code focus]
}
```

`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`.

```ts title="insert.ts"
import { db } from "./db.ts";

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

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

<Callout type="info" title="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.
</Callout>

## 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.

| Kind | Category | Status | Retries |
| --- | --- | --- | --- |
| `invalid` | `input` | 422 | no |
| `not_null` | `input` | 422 | no |
| `check` | `input` | 422 | no |
| `foreign_key` | `input` | 422 | no |
| `unique` | `conflict` | 409 | no |
| `exclusion` | `conflict` | 409 | no |
| `conflict` | `conflict` | 409 | no |
| `not_found` | `not_found` | 404 | no |
| `not_unique` | `not_found` | 404 | no |
| `forbidden` | `forbidden` | 403 | no |
| `serialization` | `transient` | 503 | yes |
| `deadlock` | `transient` | 503 | yes |
| `lock_timeout` | `transient` | 503 | yes |
| `timeout` | `transient` | 503 | yes |
| `cancelled` | `transient` | 503 | no |
| `unavailable` | `transient` | 503 | yes |
| `schema_drift` | `internal` | 500 | no |
| `driver` | `internal` | 500 | no |
| `read_only` | `internal` | 500 | no |
| `outcome_unknown` | `internal` | 500 | no |

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](/docs/define/connect/errors).

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

| Kinds | Default fix |
| --- | --- |
| `cancelled` | Do not retry. The caller aborted the call. |
| `outcome_unknown` | Check whether the write landed, using an idempotency key, before retrying. |
| `timeout`, `serialization`, `deadlock`, `lock_timeout`, `unavailable` | Retry the call. |
| `unique`, `exclusion`, `conflict` | Change the conflicting value, or update the existing row. |
| `not_null` | Provide a value for the column. |
| `check`, `invalid` | Change the value so it passes the check. |
| `foreign_key` | Use a key that exists, or insert the referenced row first. |
| `not_found` | Look up a row that exists. |
| `not_unique` | Narrow the filter so it matches one row. |
| `forbidden` | Use a role that is allowed to perform this operation. |
| `read_only` | Run the write on the primary. |
| `schema_drift` | Apply the migrations that match this build. |
| `driver` | Read 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.

| Code | Kind |
| --- | --- |
| `OKM1401` | `outcome_unknown` |
| `OKM1846` | `timeout` |
| `OKM1520` | `schema_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.

| Code | Kind |
| --- | --- |
| `OKM1522` | `conflict` |
| `OKM1825` | `forbidden` |
| `OKM1850` | `forbidden` |
| `OKM1851` | `forbidden` |

## 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.

| SQLSTATE | Kind |
| --- | --- |
| `23505` | `unique` |
| `23502` | `not_null` |
| `23514` | `check` |
| `23503`, `23001` | `foreign_key` |
| `23P01` | `exclusion` |
| `23000` | `conflict` |
| `40001` | `serialization` |
| `40P01` | `deadlock` |
| `55P03` | `lock_timeout` |
| `57014` | `timeout` |
| `40003` | `outcome_unknown` |
| `25006` | `read_only` |
| class `08`, `53300`, `57P01`, `57P02`, `57P03` | `unavailable` |
| class `22` | `invalid` |
| class `53` | `unavailable` |

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.

| SQLSTATE | Kind stays | Reason |
| --- | --- | --- |
| `42P01` | `driver` | `undefined_table` |
| `42703` | `driver` | `undefined_column` |
| `0A000`, message contains `materialized view is not populated` | `driver` | `not_populated` |
| `55000`, message contains `cannot refresh materialized view` and `concurrently` | `driver` | `no_unique_index` |
| `42501`, message contains `materialized view` | `driver` | `not_owner` |
| no SQLSTATE, TLS cause | `unavailable` | `tls` |

`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.

```text title="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

| Code | Title | Summary |
| --- | --- | --- |
| `OKM1012` | Trait field conflict | A trait added a field that the table already declares. |
| `OKM1020` | Unknown table | A table, dependency, or catalog object is missing or does not match its identity. |
| `OKM1021` | Foreign key target | A foreign key is missing its target table or column, or the target is ambiguous. |
| `OKM1022` | Foreign key type | A foreign key column and the column it references have different types. |
| `OKM1023` | Duplicate name | Two catalog objects share an identity, or two tables emit the same type name. |
| `OKM1024` | Table file not in schema | A table file is not passed to schema(). |
| `OKM1025` | Multiple Register | More than one Register augmentation is in the project. |
| `OKM1026` | Dependency cycle | The catalog dependency graph contains a cycle, so objects cannot be ordered. |
| `OKM1027` | Unreadable catalog | The catalog document is malformed or uses a version this build cannot read. |
| `OKM1030` | Validation in two places | A field is validated both on the column and somewhere else. |
| `OKM1040` | Preset name collision | A 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. |
| `OKM1051` | Archive foreign key | A foreign key into a table-strategy archivable table uses CASCADE, SET NULL, or SET DEFAULT and is not listed in cascade. |
| `OKM1052` | Not archivable | archive or restore was called on a table that is not archivable. |
| `OKM1060` | Invalid column | A column definition has a bad length, precision, scale, array rank, interval qualifier, or an empty or repeated picklist or enum. |
| `OKM1061` | Reserved option | The option exists in the types but this version does not implement it. |

### Queries and capabilities

| Code | Title | Summary |
| --- | --- | --- |
| `OKM1101` | Find without limit | find was called without a limit. |
| `OKM1102` | Unfiltered write | A write with no effective predicate, including undefined values, is OKM1102. |
| `OKM1104` | Upsert target | onConflict names columns that are not a unique constraint. |
| `OKM1105` | Include without limit | A to-many include has no limit. |
| `OKM1110` | Engine version | A feature needs a newer PostgreSQL than minPostgres allows. The message names the feature, the version it needs, and this schema's minimum. |
| `OKM1111` | Missing capability | The call needs a driver capability this driver does not have. |
| `OKM1120` | Unknown field | The 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). |
| `OKM1121` | Object as value | An empty or() branch is OKM1121. or() takes one array: or([a, b]). A plain object was passed where a value is required. |
| `OKM1122` | Bad identifier | An identifier is empty, too long, contains NUL or another control character, or is an unquoted reserved word. |
| `OKM1123` | Hidden field allowlist | A hidden field was added to a filter or sort allowlist. |
| `OKM1124` | Operator mismatch | An operator does not apply to the column type. |
| `OKM1125` | Bad SQL fragment | A 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. |
| `OKM1126` | Parameter limit | The finished statement has more than 65,535 parameters. The limit is for the whole statement (filters, inList, limit, values), not for inList alone. |
| `OKM1130` | Cursor order | A cursor was reused with a different orderBy than the one that created it, or is not a cursor page() returned. |
| `OKM1190` | Safety violation | A query composition broke a core invariant. violations lists every broken rule in stable order. |
| `OKM1191` | No snapshot plan | A multi-statement read was requested inside READ COMMITTED and no single-statement plan exists. |

### Validation

| Code | Title | Summary |
| --- | --- | --- |
| `OKM1200` | Validation failed | A write or a standalone check failed a validation rule. issues lists each path and message key. |
| `OKM1201` | Validation not imported | Validation is enabled but `okmodel/validate` was not imported. |
| `OKM1210` | Codec rejected a value | A codec refused a value: not numeric text, not a valid temporal value, or the wrong shape. |

### Transactions

| Code | Title | Summary |
| --- | --- | --- |
| `OKM1401` | Outcome unknown | A 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.

| Code | Title | Summary |
| --- | --- | --- |
| `OKM1510` | Unsafe migration | A migration has an unresolved lint error, or an override with an empty reason or a code no statement in the step triggered. |
| `OKM1511` | Drop table | A statement drops a table. The rows and anything that reads them are gone. |
| `OKM1512` | Drop column | A statement drops a column. Stored values and the code that reads them are gone. |
| `OKM1513` | Drop enum | A statement drops an enum that columns or code may still depend on. |
| `OKM1514` | Drop domain | A statement drops a domain that columns or code may still depend on. |
| `OKM1515` | Drop function | A statement drops a function that queries or triggers may still call. |
| `OKM1516` | Drop view | A statement drops a view that queries may still read. |
| `OKM1517` | Drop extension | A statement drops an extension. A dependent still in the catalog is refused before this finding. |
| `OKM1518` | Drop materialized view | A statement drops a materialized view and the rows it stores. |
| `OKM1519` | Rename column | A statement renames a column. Existing queries still use the old name. |
| `OKM1520` | Schema drift | The database has drifted past what an expand migration can repair. |
| `OKM1521` | Snapshot mismatch | Provisioning from the snapshot does not match the replayed migration history. |
| `OKM1522` | Apply lock held | Another apply already holds the target's migration lock. |
| `OKM1523` | Rename table | A statement renames a table. Existing queries still use the old name. |
| `OKM1524` | Column type change | A statement changes a column type. Existing readers and writers may not accept the new type. |
| `OKM1525` | Required column | A statement adds a NOT NULL column with no default to a table that already has rows. |
| `OKM1526` | Drop default | A statement removes a column default. Writers that omitted the column will fail. |
| `OKM1527` | Shrink length | A statement shortens a character length. Values longer than the new limit no longer fit. |
| `OKM1528` | Unique constraint | A statement adds a unique or primary-key constraint on a table that already has rows. |
| `OKM1529` | Unique index | A statement creates a unique index on a table that already has rows. |
| `OKM1530` | Ambiguous rename | A rename could match more than one object. |
| `OKM1531` | Validating check | A statement adds or validates a check against rows already stored. |
| `OKM1532` | Validating foreign key | A statement adds or validates a foreign key against rows already stored. |
| `OKM1533` | Narrowing type | A statement narrows a numeric or integer type. Values outside the new range no longer fit. |
| `OKM1534` | Index locks | A statement creates an index on an existing table without CONCURRENTLY. |
| `OKM1535` | Check without NOT VALID | A statement adds a check on an existing table without NOT VALID. |
| `OKM1536` | Foreign key without NOT VALID | A statement adds a foreign key on an existing table without NOT VALID. |
| `OKM1537` | Set not null | A statement sets NOT NULL on a column that already exists. |
| `OKM1538` | Type rewrite | A 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](/docs/reference/type-change). |
| `OKM1539` | Timestamp without time zone | A column is timestamp without time zone. The instant depends on the session time zone. |
| `OKM1540` | varchar length | A column is varchar(n). text stores the same values without a length limit in the type. |
| `OKM1541` | Picklist removal | A picklist or enum value was removed and the replacement was not given. |
| `OKM1542` | Data statement | A migration contains a data statement outside backfill(), or a DO or CALL whose body is not analysed. |
| `OKM1543` | serial column | A column uses serial or a nextval default. Identity is the form this repo generates. |
| `OKM1544` | json column | A column is json. jsonb is available and stores the same documents in binary. |
| `OKM1545` | Identity not always | An identity column is generated by default. The repo default is generated always. |
| `OKM1546` | Backfill key | A backfill step targets a table that has no primary key. |
| `OKM1547` | History differs | Replaying a migration does not match its stored catalog, or the file was generated from a different parent than the previous migration. |
| `OKM1548` | Previous catalog | An expand migration drops or changes a table, column, constraint, or type the previous catalog relies on. |
| `OKM1549` | Stale head | The last migration's catalog does not match the schema. |
| `OKM1550` | Row-level security weakened | A statement disables row-level security, drops or widens a policy, grants BYPASSRLS, or changes the owner of a tenant table. |
| `OKM1551` | Transaction control in a step | A step contains COMMIT, ROLLBACK, or other transaction control, including inside a DO body. A function or procedure body is not this. |
| `OKM1552` | Reprinted declaration | A domain check or a LANGUAGE sql function body is stored differently from the text in the migration catalog. |

### Typed SQL

| Code | Title | Summary |
| --- | --- | --- |
| `OKM1601` | Stale typed SQL | A typed SQL signature no longer matches the database. Recording signatures with `okm build` is deferred. |
| `OKM1602` | Bad typed SQL | A raw statement is missing its class, is not one select or one write, or the result class has no column initializers. |

### Tenancy and policies

| Code | Title | Summary |
| --- | --- | --- |
| `OKM1701` | Tenant context missing | A tenant table was used on a client that has no tenant context. |
| `OKM1702` | Unverifiable tenant SQL | Raw 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. |
| `OKM1704` | Tenant key change | An update tried to change the tenant key. |
| `OKM1705` | Global references tenant | A global table references a tenant table. |
| `OKM1706` | Tenant index | An index on a tenant table does not lead with the tenant key. |
| `OKM1707` | RLS role | A 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. |
| `OKM1708` | RLS policy | A 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

| Code | Title | Summary |
| --- | --- | --- |
| `OKM1801` | Dialect mismatch | The schema dialect and the driver dialect are not the same. |
| `OKM1802` | Server version | The connected server is older than this schema's minPostgres. |
| `OKM1803` | Postgres floor | The connected server is older than PostgreSQL 15. |
| `OKM1810` | Extension not declared | An extension builder was used but the extension is not declared on the schema. |
| `OKM1811` | Extension unavailable | A declared extension is not installed on the server. |
| `OKM1812` | Feature version | A feature needs a newer PostgreSQL than minPostgres, or an extension version the server does not have. The message names the feature and the version. |
| `OKM1813` | Extension defined twice | The same extension definition was registered twice. |
| `OKM1814` | Extension change refused | The plan would downgrade an extension, move a non-relocatable one, or drop one that still has dependents. |
| `OKM1820` | View tenancy | A view over tenant tables does not expose the tenant key and is not marked global. |
| `OKM1821` | Replace needs recreate | An incompatible replace would have to recreate dependents. CASCADE is never used. |
| `OKM1822` | Concurrent refresh | REFRESH CONCURRENTLY was declared without a unique index on the materialized view. |
| `OKM1823` | Security definer path | A SECURITY DEFINER function has no search_path. |
| `OKM1824` | Function dependencies | A plpgsql function has no dependsOn. Postgres does not record dependencies inside the body. |
| `OKM1825` | Missing privilege | A role lacks a privilege on a managed object. |
| `OKM1830` | Lock outside transaction | find was called with a row lock outside tx(). |
| `OKM1840` | Replica not allowed | A replica route was used on an operation that requires the primary, or inside tx(). |
| `OKM1841` | External object differs | An external object does not match the owner's exported catalog. |
| `OKM1842` | Cross-database foreign key | A foreign key points at a table in another database. |
| `OKM1843` | No eligible replica | A replica was required and none is eligible: unhealthy, behind the session, or none configured. |
| `OKM1844` | Fallback refused | An automatic read found no eligible replica and fallback is error. |
| `OKM1845` | Target unresolved | The target could not be resolved. The tenant is unknown or the resolver failed. |
| `OKM1846` | Acquire timeout | The pool did not hand out a connection within timeouts.acquire. |
| `OKM1850` | Protected target | The operation is blocked on a protected target and --allow-protected was not passed. |
| `OKM1851` | Target not empty | Provisioning was asked to fill a target that already has objects. |
| `OKM1852` | Protection mismatch | Targets that resolve to the same database disagree about protection, or tenants of one schema-per-tenant database do. |
| `OKM1853` | Target not named | Several targets are configured and the command was not given --target. |
| `OKM1854` | Pooler session | Two statements on the same connection saw different backend pids. A transaction-mode pooler is in the way. |
| `OKM1855` | Pull would overwrite | okm pull or okm import drizzle found a file it would replace. It writes nothing. |
| `OKM1856` | Drizzle is not installed | okm import drizzle could not resolve drizzle-orm from the project. Install it in that project. |


# GitHub Action (/docs/reference/github-action)

The composite action `.github/actions/migrate` runs `okm migrate check` and `okm migrate plan --json` against a named target and posts one sticky pull-request comment. The heading is the strictest class. Each step shows its class and lock, including the row estimate the plan prints. Lint findings keep their codes. A lint error or a failed check fails the job with that command's exit code.

`preview: true` runs `okm migrate apply` on the target you named, then `okm seed` when `seed` is set. The action does not infer the target. A protected target is refused by the existing policy. The action never creates or drops a database.

## Permissions

The comment step uses the workflow `GITHUB_TOKEN` only.

```yaml
permissions:
  contents: read
  pull-requests: write
```

Pass the database URL as `DATABASE_URL`. A URL in the `database-url` input can show up in the workflow file. The action masks `DATABASE_URL` in the log and does not echo it.

A fork pull request receives no secrets. Preview mode is for a pull request in the same repository.

## Plan

`version` empty runs the okmodel source checked out with the action. Set `version` to a published release that supports `okm migrate plan --json` to install that package instead. `comment` defaults to `true`. The comment contains `<!-- okmodel-migrate-plan -->` and is updated in place.

```yaml
name: Plan
on:
  pull_request:
permissions:
  contents: read
  pull-requests: write
jobs:
  plan:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: omqkhafi/okmodel/.github/actions/migrate@<commit>
        with:
          target: ci
        env:
          DATABASE_URL: ${{ secrets.CI_DATABASE_URL }}
```

Pin `<commit>` to the full SHA of the release you trust. The action pins `oven-sh/setup-bun` by SHA.

## Preview

Create the empty database outside the action. `okm migrate apply --target preview` installs the head snapshot. `okm seed` runs when `seed` is set. Deleting the database stays outside the action.

```yaml
- uses: omqkhafi/okmodel/.github/actions/migrate@<commit>
  with:
    target: preview
    preview: "true"
    seed: seed.ts
  env:
    DATABASE_URL: ${{ secrets.PREVIEW_DATABASE_URL }}
```


# OKID (/docs/reference/okid)

`okid()` is a client generator. The id column is `text` with collation `C`.

`okid` comes from `okmodel/ids`. Calling it returns the generator. Insert calls that generator once when the field is omitted and stores the string. The catalog stores no default.

<Callout type="info" title="Where this fits">
The column form is on [columns](/docs/define/table/columns#okid). A schema-wide id is the [id default](/docs/define/schema/defaults). [`generators`](/docs/define/connect/generators) replaces the function on `connect`.
</Callout>

### Format

A default id is 21 characters from a 64-character alphabet: digits, `a-z`, `A-Z`, `-`, and `_`. That is 126 bits. The characters are URL-safe. `length` is the body, an integer from 8 to 128. The prefix is extra.

| Call | Body |
| --- | --- |
| `okid()` | 21 characters. |
| `okid(length)` | `length` characters. |
| `okid({ sortable: true })` | 8 time characters, then random. Length is at least 16. |
| `okid({ prefix })` | The prefix, then the body. The prefix is at most 32 characters. |

The 8 time characters are a 48-bit epoch millisecond, so string order matches time across milliseconds. Two ids from the same millisecond share those 8 characters. The rest is random. With a prefix, a sortable id is the prefix, then those 8 characters, then the random tail. Collation `C` keeps that order as byte order.

`lowercase`, `uppercase`, `numbers`, and `symbols` default to included. `lookAlikes: false` drops `1`, `l`, `I`, `0`, `O`, `o`, `u`, `v`, `5`, `S`, and `s` from the body. A sortable id ignores those toggles. A smaller alphabet still draws each remaining character equally. The draw uses `crypto.getRandomValues`.

<Callout type="warning" title="Not a secret">
An OKID identifies a row. A sortable id embeds its creation time, to the millisecond.
</Callout>

### Options

| Field | Default | Effect |
| --- | --- | --- |
| `length` | 21 | Body length, from 8 to 128. Sortable needs at least 16. |
| `prefix` | none | Prepended. At most 32 characters from the alphabet. |
| `sortable` | false | Time in the first 8 characters of the body. |
| `lowercase` | true | Include `a-z`. |
| `uppercase` | true | Include `A-Z`. |
| `numbers` | true | Include `0-9`. |
| `symbols` | true | Include `-` and `_`. |
| `lookAlikes` | true | `false` drops the characters above from the body. |

<Callout type="error" title="RangeError">
A bad option throws when the generator runs. A fractional length is `okid: length must be an integer, got 1.5` Length 7 is `okid: length 7 is out of range [8, 128]` A sortable length of 15 is `okid: length 15 is out of range [16, 128]` An empty alphabet is `okid: alphabet is empty — enable at least one character group` A long prefix is `okid: prefix length 33 exceeds max 32` A character outside the alphabet is `okid: prefix contains invalid character ":" — use characters from OKID_ALPHABET` That name is the generator's alphabet. `okmodel/ids` exports `okid` and the `OkidOptions` type.
</Callout>

### Column

`t.id({ default: okid() })` is a primary key. Insert omits the field. Update cannot set it. A bare `t.id()` stays `uuid` until the schema [id default](/docs/define/schema/defaults) is an `okid` generator. A column option wins.

`.default(okid())` on another string column fills that column in the client. The SQL type and collation stay as declared. Collation `C` is set when the generator name on `t.id` is `okid`.

A writer that bypasses okmodel must supply the value. The column has no database default.

```ts title="users.ts"
import { okid } from "okmodel/ids";
import { table, t } from "okmodel/pg";

export const users = table("users", {
  id: t.id({ default: okid({ prefix: "usr_", sortable: true }) }), // [!code focus]
  email: t.text(),
});
```

```ts title="schema.ts"
import { okid } from "okmodel/ids";
import { schema, table, t } from "okmodel/pg";

const users = table("users", {
  id: t.id(), // [!code focus]
  name: t.text(),
});

export default schema({
  tables: [users],
  defaults: { id: okid({ prefix: "usr_" }) }, // [!code focus]
});
```

<Callout type="error" title="OKM1190">
The id is guarded. `Field users.id is guarded. Input cannot set it.`
</Callout>

### Catalog and migrations

The catalog stores `text` and collation `C`. Create SQL includes `collate "C"` and no default. The model labels the fill `client`. The hashed catalog does not store the generator, so a new prefix, length, sortable flag, or alphabet toggle does not change the catalog hash and does not produce a migration.

A change of SQL type or collation does. The plan emits `set data type text collate "C"`. A type change adds `using "id"::text`. A database default that goes away is `drop default`.

Storage follows [text](/docs/define/table/columns#text). A 21-character body is 22 bytes.

### Generators

`connect({ generators: { okid } })` replaces every `okid` generator. The function takes no arguments. One function covers every column of that name. The string it returns is the stored id. A function you wrote and passed to `.default()` is left alone.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { okid } from "okmodel/ids";

const notes = table("notes", {
  id: t.id({ default: okid({ prefix: "nt_" }) }),
  title: t.text(),
});
const app = schema({ tables: [notes] });

export async function add(url: string, title: string) {
  await using db = connect(url, {
    schema: app,
    generators: {
      okid: () => "nt_fixed", // [!code focus]
    },
  });
  return db.notes.insert({ title });
}
```

### Drift

The implementation is a vendored copy of okengine: `okid.ts`, `okid-shared.ts`, and `okid-extended.ts` under `src/runtime/ids`. The copy is Apache-2.0. okengine stays the source of truth. `bun run okid-drift` fails when those files leave the hashes in `src/runtime/ids/okid.vendor.json`. The check is part of `bun run check`.

When okengine is checked out beside this repo, or `OKENGINE_SRC` points at its `src` directory, the check compares that tree as well. The copy may import the options path directly, so this package does not depend on okengine. The `okid` exported from `okmodel/ids` is the generator around that copy.


# Schema API (/docs/reference/schema-api)

`describe` and `buildCatalog` live on `okmodel/migrate`. A codegen tool, a CI check, or a dashboard imports them. They are not on the runtime entry, and `schema()` does not change.

`describe(schema)` returns JSON. `logical` is the schema identity: `namespace`, `name`, and `format`. That identity is `public`, the namespace `schema()` compiles. `defineConfig({ namespace })` places the catalog into another Postgres schema. That placement changes the catalog hash and does not change `logical`. `catalog` is the catalog document. Object keys are sorted. There are no functions in it.

`okm catalog export` (including `--json`) is a different artifact: the column snapshot a dependent verifies (table names plus each column's name, type, and nullability). It is not this description.

`buildCatalog` reads a description, including one that came back from `JSON.parse`. The catalog hash matches the catalog `schema()` built.

Codecs, client generators (`uuidv4`, `uuidv7`, `okid`), and relation helpers stay on the schema. The catalog hash does not include them.

```ts title="describe.ts"
import { schema, t, table } from "okmodel/pg";
import { buildCatalog, describe } from "okmodel/migrate";

const users = table("users", {
  id: t.id(),
  email: t.text(),
});

export const app = schema({ tables: [users] });

export const description = describe(app);

export const catalog = buildCatalog(JSON.parse(JSON.stringify(description)));
```

`catalog` is the catalog `okm` plans. Apply that catalog the same way as `app.catalog`.


# Type change (/docs/reference/type-change)

OKM1538 warns when a column type change rewrites the table. 0.9 does not rewrite that statement into a safe form, and there is no `okm migrate type-change` command. Add a new column, fill it, keep both columns updated, switch readers, then drop the old column. Each of those is its own migration.

`varchar(n)` widening, `varchar` to `text`, and the other changes listed on [OKM1538](/docs/reference/errors) do not rewrite the heap. This page is for a change that does, such as `integer` cents to `numeric(12,2)` dollars.

## Add the new column

The new column is nullable. The release that still writes `cents` keeps working.

```sql
alter table "public"."prices" add column "amount" numeric(12, 2);
```

## Backfill

The step header is `-- backfill`. The statement fills rows where `amount` is still null, one key range at a time. Apply runs one transaction per batch.

```sql
-- backfill table="public"."prices" key="id" batch=1000
-- transactional: false
update "public"."prices"
set "amount" = "cents"::numeric / 100
where "amount" is null
  and ($1::text is null or "id" > $1::integer)
  and ($2::text is null or "id" <= $2::integer);
```

## Dual-write

A `before insert or update` trigger copies a write on one column onto the other. Deploy it before any release that writes `amount`. An old release that sets only `cents` still fills `amount`. A new release that sets only `amount` still fills `cents`.

```sql
create function "public"."prices_amount_sync"() returns trigger
language plpgsql as $$
begin
  if tg_op = 'INSERT' then
    if new.amount is null and new.cents is not null then
      new.amount := new.cents::numeric / 100;
    elsif new.cents is null and new.amount is not null then
      new.cents := (new.amount * 100)::integer;
    end if;
    return new;
  end if;
  if new.cents is distinct from old.cents and new.amount is not distinct from old.amount then
    new.amount := new.cents::numeric / 100;
  elsif new.amount is distinct from old.amount and new.cents is not distinct from old.cents then
    new.cents := (new.amount * 100)::integer;
  end if;
  return new;
end;
$$;

create trigger "prices_amount_sync"
before insert or update on "public"."prices"
for each row execute function "public"."prices_amount_sync"();
```

## Switch reads

Ship the release that reads `amount` with this migration, after the backfill has finished. The check is added `not valid`, validated, then the column is `not null`, then the check is dropped. The trigger stays, so an old writer still fills `amount`.

```sql
alter table "public"."prices"
  add constraint "prices_amount_not_null" check ("amount" is not null) not valid;
alter table "public"."prices" validate constraint "prices_amount_not_null";
alter table "public"."prices" alter column "amount" set not null;
alter table "public"."prices" drop constraint "prices_amount_not_null";
```

## Drop the old column

Ship this only after every release reads and writes `amount`. Dropping the column is OKM1512, so the step needs a reason. Drop the trigger first.

```sql
-- okm-allow OKM1512: readers and writers use amount
drop trigger "prices_amount_sync" on "public"."prices";
drop function "public"."prices_amount_sync"();
alter table "public"."prices" drop column "cents";
```


# Basic Usage (/docs/start/basic-usage)

`db` is the client from [Let's start](/docs/start). Each section is one call.

## Insert

Insert passes `title` and omits `id`. The row that comes back has `id`.

```ts title="notes.ts"
import { db } from "./db.ts";

export async function createNote(title: string) {
  const note = await db.notes.insert({ title }); // [!code focus]
  return note;
}
```

## Find

`find` returns a list. `limit: 1` asks for one row. The row is `[0]`.

```ts title="notes.ts"
import { db } from "./db.ts";

export async function readNote(id: string) {
  const found = await db.notes.find({ where: { id }, limit: 1 }); // [!code focus]
  return found[0]; // [!code focus]
}
```

Omit the limit and the call is OKM1101. `[0]` is `undefined` when nothing matched. `.all(reason)` is how you ask for the full set.

## One

`one` returns that row, or `null` when nothing matched. More than one match throws `not_unique`.

```ts title="notes.ts"
import { db } from "./db.ts";

export async function readOne(id: string) {
  return db.notes.one({ where: { id } }); // [!code focus]
}
```

## Count

`count` returns how many rows match. It does not return the rows.

```ts title="notes.ts"
import { db } from "./db.ts";

export async function countNotes() {
  return db.notes.count(); // [!code focus]
}
```

## Exists

`exists` returns true when a row matches. It does not return the row.

```ts title="notes.ts"
import { db } from "./db.ts";

export async function hasNote(id: string) {
  return db.notes.exists({ where: { id } }); // [!code focus]
}
```

## Update

`where` picks the row. `set` names the new title. A write with no `where` is OKM1102.

```ts title="notes.ts"
import { db } from "./db.ts";

export async function renameNote(id: string, title: string) {
  await db.notes.update({ where: { id }, set: { title } }); // [!code focus]
}
```

## Delete

`where` picks the row. A write with no `where` is OKM1102.

```ts title="notes.ts"
import { db } from "./db.ts";

export async function removeNote(id: string) {
  await db.notes.delete({ where: { id } }); // [!code focus]
}
```


# From Drizzle (/docs/start/from-drizzle)

`okm import drizzle` reads a Drizzle schema and writes one table file per table, plus the schema file. It does not need a database. The file is imported, so it runs. `drizzle-orm` is resolved from the project. okmodel does not depend on it.

```sh
bunx okm import drizzle ./src/db/schema.ts --out ./adopted
```

An existing file is OKM1855 and nothing is written. A project that cannot resolve `drizzle-orm` is OKM1856. Install it in that project.

The report starts with `namespace <name>`. SQL names stay. TypeScript fields follow the Drizzle property names. File stems are unique when folded to lowercase, and a Windows-reserved stem gets a `_` prefix, the same rules as `okm pull`. Composite primary-key columns are not null. When the namespace is not `public`, set it on the config. `schema()` still compiles `public`.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./adopted/schema.ts",
  tables: "./adopted/tables",
  namespace: "shop",
  database: process.env.DATABASE_URL!,
});
```

Then `okm build` and `okm generate init`.

These are printed under `not imported`, with the table and the reason, and they are not written:

- A composite foreign key. `references()` takes one local column.
- `relations()`. They are not guessed into foreign keys.
- A custom column type.
- A view, a policy, and a sequence `table()` cannot map.
- The sequence behind `serial`, `smallserial`, or `bigserial`. The column is written as `integer`, `smallint`, or `bigint` and not null.

A second Postgres schema is listed the same way. Import it on its own and set `namespace`.


# Let's start (/docs/start)

<Steps>

<Step>
### Install

`okmodel` has no runtime dependencies. Each driver is an optional peer. Pick the one the runtime already has.

<Tabs items={["postgres.js", "node-postgres", "PGlite", "Bun.sql"]}>

<Tab value="postgres.js">

The default on Node and Bun. It connects to a Postgres server.

```sh title="postgres.js"
bun add okmodel postgres
```

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
```

</Tab>

<Tab value="node-postgres">

The other server driver on Node and Bun, when `pg` is already installed. `pg` ships no types, so `@types/pg` is a dev dependency.

```sh title="node-postgres"
bun add okmodel pg
bun add -D @types/pg
```

```ts title="db.ts"
import { connect } from "okmodel/pg/pg";
```

</Tab>

<Tab value="PGlite">

Postgres runs in the process, on Node or Bun. There is no server to start. The connect target is a data directory, or omit it for memory.

```sh title="PGlite"
bun add okmodel @electric-sql/pglite
```

```ts title="db.ts"
import { connect } from "okmodel/pg/pglite";
```

</Tab>

<Tab value="Bun.sql">

Built into Bun. No extra package. This entry does not run on Node. `bunx okm` still uses Node. `bun --bun okm` is the Bun runtime.

```sh title="Bun.sql"
bun add okmodel
```

```ts title="db.ts"
import { connect } from "okmodel/pg/bun";
```

</Tab>

</Tabs>

</Step>

<Step>
### Define the table

`table()` stores the columns. `t.identity().primaryKey()` makes `id` the primary key.

```ts title="schema.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity().primaryKey(),
  title: t.text(),
});
```

`t.identity()` is a `bigint`, generated always. Insert omits `id`. The default codec is a string. A key of more than one column is the `primaryKey` option. [Keys](/docs/define/table/keys).

</Step>

<Step>
### Define the schema

`schema()` compiles the tables into one catalog. The module default-exports that result.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity().primaryKey(),
  title: t.text(),
});

export default schema({ tables: [notes] }); // [!code focus]
```

</Step>

<Step>
### Define the config

Point `schema` at the file from the previous step. `database` is the URL push connects to.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts", // [!code focus]
  database: process.env.DATABASE_URL!, // [!code focus]
});
```

`okm push` loads `okmodel.config.ts` from the project directory. The file default-exports `defineConfig`. A file that does not is refused. A module that is not a `schema()` is refused: `<path> must export a schema().` Other options are on [Config](/docs/define/config).

</Step>

<Step>
### Push

`okm push` connects and creates the tables. It does not write a SQL file.

```sh
bunx okm push
```

A protected target refuses push (OKM1850). No schema change prints `no changes`. `bunx okm` runs with Node. `bun --bun okm` uses the Bun runtime.

</Step>

<Step>
### Connect

`connect` takes the direct URL and the schema. The process keeps `db`. Direct asserts the URL. Safe refuses an empty `DATABASE_URL`. The options are on [Connect](/docs/define/connect).

<Tabs items={["Direct", "Safe"]}>

<Tab value="Direct">

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";

import schema from "./schema.ts";

export const db = connect(process.env.DATABASE_URL!, { schema }); // [!code focus]
```

</Tab>

<Tab value="Safe">

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";

import schema from "./schema.ts";

const url = process.env.DATABASE_URL;
if (url === undefined || url.length === 0) throw new Error("DATABASE_URL is not set"); // [!code focus]

export const db = connect(url, { schema });
```

</Tab>

</Tabs>

</Step>

<Step>
### 🎉 That's it!

That's it! You're now ready to use okmodel in your application. Continue to [Basic Usage](/docs/start/basic-usage) to learn how to insert, find, update, and delete.
</Step>

</Steps>


# Check (/docs/define/connect/check)

`schema` is required. The [schema](/docs/define/schema) types the client. `db.connected` resolves when the check finishes. A query waits for it. A transaction and a batch wait too.

[minPostgres](/docs/define/schema/min-postgres) is the floor `connect` reads. With no floor, the minimum is 15. A named floor is 15, 16, 17, or 18. A server below that major is refused. A major below 15 is refused when `schema()` runs. The floor runs after the hash check.

`signal` and `timeout` on this check are [Timeouts](/docs/define/connect/session#timeouts). A preset name that is a client method or reserved is checked before the first query. The names are on [Presets](/docs/define/table/presets).

| Result | What happens |
| --- | --- |
| Not PostgreSQL | OKM1801 |
| `okm_meta` has no hash, and `requireMeta` is set | OKM1520 |
| Equal catalog hashes | Returns without reading `okm_history`. |

`okm_meta` is read in the namespace `defineConfig` placed. `public` uses the unqualified name. `okm build` writes `.okm/namespace` for any other schema, and `connect` reads that file from `catalogDir` or `.okm`. A missing file keeps the `public` query.
| Ahead by expand migrations | The drift check returns. The client opens when the floor passes. |
| Ahead by a contract migration, or behind | OKM1520 |
| Below 15, and the schema names no `minPostgres` | OKM1803 |
| Below the schema `minPostgres` | OKM1802 |

### schema

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] }); // [!code focus]

export const db = connect(process.env.DATABASE_URL!, { schema: app }); // [!code focus]
```

### server

`version()` must start with `PostgreSQL`.

<Callout type="error" title="OKM1801">
`The server is not PostgreSQL. The schema dialect is postgres.`
</Callout>

### floor

A server below 15, when no floor is named, is OKM1803.

<Callout type="error" title="OKM1803">
`The server is PostgreSQL 14. This schema needs PostgreSQL 15 or higher (minPostgres: 15).`
</Callout>

A server below the named floor is OKM1802.

<Callout type="error" title="OKM1802">
`The server is PostgreSQL 16. This schema needs PostgreSQL 17 or higher (minPostgres: 17).`
</Callout>

A value that is not an allowed major is refused when `schema()` runs. [Minimum Postgres](/docs/define/schema/min-postgres) lists the majors. `requires: { postgres: ">=17" }` is the same floor and is deprecated.

### Require meta

`requireMeta` fails `connect` when `okm_meta` has no catalog hash. It is a `connect` option. The target name and `NODE_ENV` do not turn it on. Set this on a production connection. Only `true` turns it on. `false` is the same as omitted. `protected` on a migrate target does not affect `connect()`.

| Value | Result |
| --- | --- |
| omitted | A database with no recorded hash skips the drift check. An existing database can adopt OKModel. |
| `true` | A missing hash is OKM1520. |

Leave `requireMeta` out. A database with no recorded hash skips the drift check. A null hash and an empty hash are both no catalog hash. An existing database can adopt OKModel. The version check still runs.

A recorded hash still runs the drift check, below.

`requireMeta: true` fails `connect` when the hash is missing. That throw is OKM1520. It happens before the drift check and before the version check.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, {
  schema: app,
  requireMeta: true, // [!code focus]
});
```

<Callout type="error" title="OKM1520">
A missing hash is `okm_meta has no catalog hash.` The fix is `Apply migrations so okm_meta records the catalog, or omit requireMeta to adopt this database.`
</Callout>

### Catalog

`catalog` and `catalogDir` exist so this check can load the code's catalog. The value is `{ text, hash }`. `text` is the catalog JSON. `hash` is that hash. The JSON is checked only when the database hash differs.

Connect takes the first row that applies.

| Source | Code hash |
| --- | --- |
| `catalog` | `hash` on `{ text, hash }`. |
| a non-empty `catalog.hash` | The file. A mismatch reads `catalog.json` once per process. |
| a missing or empty `catalog.hash` | The schema catalog. |

When `catalog` is set, `hash` is the code's catalog hash. A difference loads `text` once per process. The load checks `text` against `hash`. Concurrent connects share that load.

`catalogDir` is the directory of `catalog.hash` and `catalog.json`. A trailing `/` or `\` is removed, so the joined file name is one path. Connect reads this directory when `catalog` is omitted. [`okm build`](/docs/define/config/out) writes those two files.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, {
  schema: app,
  catalogDir: ".okm", // [!code focus]
});
```

Omitted, the directory is `.okm` under the process cwd. When `process.cwd` is missing, the directory is `./.okm`.

A missing hash file hashes the schema catalog instead. `catalog.json` stays unread.

When `catalog` is omitted, connect reads `catalog.hash` from that directory. UTF-16 throws OKM1027 before the schema catalog is hashed. A leading BOM is stripped. Carriage returns are removed. The hash is then trimmed, so a trailing newline is dropped. An empty result hashes the schema catalog instead.

A hash that remains is the fast path. That file is the code's catalog hash. Equal hashes return. `okm_history` and `catalog.json` stay unread. A difference reads `catalog.json` once per process. Concurrent connects share that read. Connect does not rebuild the catalog from the schema.

<Callout type="error" title="OKM1027">
A missing `catalog.json` is `.okm/catalog.json is missing.` when `catalogDir` is `.okm`. Omitted, the directory in that message is `.okm` under the process cwd. The fix is `Run okm build so catalog.json matches catalog.hash. connect does not rebuild the catalog from the schema.`
</Callout>

<Callout type="error" title="OKM1027">
A hash that does not match the JSON is `Catalog hash does not match the build artifact.`
</Callout>

<Callout type="error" title="OKM1027">
UTF-16 `catalog.hash` is `catalog.hash is UTF-16. Save the file as UTF-8.`
</Callout>

### hash

With no hash, and without [Require meta](#require-meta), the drift check is skipped. The floor still runs. Equal catalog hashes return without reading `okm_history`. The floor still runs. A different hash loads the [catalog](#catalog). The check then reads `okm_history`.

Ahead by expand migrations, the drift check returns. The client opens when the floor passes.

Any other verdict is OKM1520. The id is the migration id from `okm_history`.

| Verdict | Message |
| --- | --- |
| ahead by contract | `The database is ahead by contract migration 0002_expand.` |
| behind by expand | `The database is behind by expand migration 0003_expand.` |
| behind by contract | `The database is behind by contract migration 0002_contract.` |
| behind | `The database is behind the app at migration 0001_old.` |
| behind, no id | `The database is behind the app.` |

A failed read of `okm_history` uses `The database is behind the app.` A schema catalog hash that differs is this comparison.

### connected

A query waits for `db.connected`. Await it when the check must finish before [`close()`](/docs/define/connect#close).

`close()`, before the check settles, on a client that opened the pool, rejects `connected` when the check fails. The message is `The client was closed before it connected. Await connected before close(), or do not close it before use.`


# Drivers (/docs/define/connect/drivers)

Import `connect` from the driver you installed. The calls on the client do not change.

| Import | Driver | Returns |
| --- | --- | --- |
| `okmodel/pg/postgresjs` | postgres.js | The client. A topology is a promise. |
| `okmodel/pg/pg` | node-postgres | The client. A topology is a promise. |
| `okmodel/pg/bun` | Bun.sql | The client. A topology is a promise. Another runtime cannot load this entry. |
| `okmodel/pg/pglite` | PGlite | A promise. Opening the database is async. |
| `okmodel/pg/neon` | Neon | A promise. Opening the pool is async. |

A string or a pool returns the client for postgres.js, node-postgres, and Bun.sql. `{ primary, replicas }` returns a promise. That object is [Topology](/docs/define/connect/topology). PGlite returns a promise for a string, a pool, omitted memory, and a topology. Neon returns a promise for a string, a pool, and a topology.

When the schema names no floor, a server below 15 is OKM1803. [minPostgres](/docs/define/schema/min-postgres) is that floor. The test suite runs on 15, 16, 17, and 18.

### postgres.js

`describe` is true. `cancel` is true. The peer package is `postgres`.

```ts title="postgresjs.ts"
import { connect } from "okmodel/pg/postgresjs"; // [!code focus]
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, { schema: app });
```

### node-postgres

`describe` is false. It cannot describe a statement without running it. `cancel` is true. The peer package is `pg`.

```ts title="pg.ts"
import { connect } from "okmodel/pg/pg"; // [!code focus]
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, { schema: app });
```

### Bun.sql

Another runtime cannot load the `bun` import. There is no package to install. `describe` is false. `RAISE NOTICE` is not surfaced. `cancel` is false.

```ts title="bun.ts"
import { connect } from "okmodel/pg/bun"; // [!code focus]
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, { schema: app });
```

### PGlite

PGlite runs in this process. The pool size is one. A statement issued while a `tx()` is open waits until that transaction ends, unless `timeouts.acquire` is set. `describe` is true. `cancel` is false. The peer package is `@electric-sql/pglite`. A string is a data directory. A pool and omitted memory are promises too. Omitted opens `memory://`.

```ts title="pglite.ts"
import { connect } from "okmodel/pg/pglite"; // [!code focus]
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = await connect("./data", { schema: app }); // [!code focus]
```

### Neon

The peer package is `@neondatabase/serverless`. `connect` and `open` return a promise. `mode` is `websocket` or `http`. Omitted is `websocket`.

WebSocket mode is a pool. `transactions` is `interactive`, so `tx()` works on a direct connection. `prepared` is `unnamed`. `stream`, `listen`, `cancel`, and `describe` are false. A positive `timeout` is refused.

HTTP mode sends one request per statement. `transactions` is `batch`. `batch()` is one atomic HTTP transaction. `tx()` is OKM1111. There is no `reserve`. `prepared` is `none`.

A transaction-mode pooler does not promise session state. Advisory locks, `SET`, `LISTEN`, and prepared statements belong to one server session. With one small compute, that pooler reused a backend: a custom `SET`, `LISTEN`, and `PREPARE` were still visible on the next statement, and a second connection saw the `SET`. That reuse is not a guarantee. Do not rely on it. `prepared: "named"` is not for that pooler.

A compute can be suspended. The first connection is a cold start. OKModel does not treat that wait as a query result.

A read replica can be behind the primary. After a write, a later read on that replica can miss the row until the replica replays it. `route: "replica"` still goes to a replica. A miss is a miss. OKModel does not report the row as present, and it does not move that call to the primary. Session consistency can keep an automatic read on the primary until the replica has caught up (`fallback:behind` or `fallback:position-unknown`).

`okm driver test neon` marks COMMIT loss `unsupported` (`no socket to cut`). The harness cut is a TCP relay, and this driver speaks WebSocket or HTTP. A result that never arrives is still `outcome_unknown` (OKM1401). Neon has its own connect gate and is not a column in the compatibility table. postgres.js and node-postgres pointed at a Neon endpoint stay unclassified.

```ts title="neon.ts"
import { connect } from "okmodel/pg/neon"; // [!code focus]
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = await connect(process.env.DATABASE_URL!, { schema: app }); // [!code focus]
```

### timeout

Bun.sql, PGlite, and Neon refuse a positive `timeout` on a call, on `tx()`, and on `batch()`. The keys and the messages are on [Timeouts](/docs/define/connect/session#timeouts).

### A lost connection

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 reserved Bun.sql handle whose backend was killed fails the next statement with `unavailable` instead of running it on a new connection. On postgres.js, a wait with no call deadline is rejected when every slot in that pool has closed, or when the reserved connection it uses closes. A queued reserve is started again after a close. A wait that already has a deadline ends at that deadline. A kill during connect can still surface an unhandled rejection from the driver's own type-catalog query.


# Errors (/docs/define/connect/errors)

`errors.http` sets the statuses `OkmError.toHttp` uses.

`toHttp()` uses these when the caller does not pass statuses. `toHttp(statuses)` replaces them for that call. A category absent from the map in use keeps the default.

| Category | Status |
| --- | --- |
| `input` | 422 |
| `conflict` | 409 |
| `not_found` | 404 |
| `forbidden` | 403 |
| `transient` | 503 |
| `internal` | 500 |

| Field | Result |
| --- | --- |
| `errors.http` | Statuses for `OkmError.toHttp`. `toHttp()` uses these when the caller does not pass statuses. `toHttp(statuses)` replaces them for that call. |
| `errors.includeValues` | `true` stores row values on the error. They are never logged. |
| `logger.error` | Receives `{ code, summary }`. |

### Statuses

The keys are `input`, `conflict`, `not_found`, `forbidden`, `transient`, and `internal`. Each value is a status for that category. One key is enough.

`connect` stores the map on the error. `toHttp()` reads it. `toHttp(statuses)` uses the object from that call. A category absent from that object takes the default status in the table.

### Row values

`includeValues: true` stores row values on the error. `values()` returns them, or `undefined` when none were kept. Omitted, or `false`, stores none.

<Callout type="info" title="Never logged">
The log record has no row values. `logger.error` receives `{ code, summary }`. The `toHttp` body is `{ code, reason, fields }`.
</Callout>

### Logger

`logger` is the option next to `errors`. `error` is optional. The argument is `{ code: string; summary: string }`.

### Client

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, {
  schema: app,
  errors: {
    http: { conflict: 409 }, // [!code focus]
    includeValues: true, // [!code focus]
  },
  logger: {
    error(entry) {
      void entry.code; // [!code focus]
      void entry.summary; // [!code focus]
    },
  },
});
```


# Generators (/docs/define/connect/generators)

`generators` replaces a built-in client generator by name. The names are `uuidv4`, `uuidv7`, and `okid`. Each replacement returns a string. A [generator](/docs/define/table/defaults#generator) runs when insert omits the field.

| Key | Insert |
| --- | --- |
| omitted | Calls the function stored on the column. |
| `uuidv4` | Calls the function you passed, for a `uuidv4` generator. |
| `uuidv7` | Calls the function you passed, for a `uuidv7` generator. |
| `okid` | Calls the function you passed, for an `okid` generator. |

The function takes no arguments. One function covers every column of that name. A bare `t.id()` that took that generator from the schema [id default](/docs/define/schema/defaults) uses it too. Each missing field calls the function once.

### uuidv7

Insert stores this string when `id` is omitted.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { uuidv7 } from "okmodel/ids";

const notes = table("notes", {
  id: t.uuid().default(uuidv7),
  title: t.text(),
});
const app = schema({ tables: [notes] });

export async function add(url: string, title: string) {
  await using db = connect(url, {
    schema: app,
    generators: {
      uuidv7: () => "00000000-0000-7000-8000-000000000000", // [!code focus]
    },
  });
  return db.notes.insert({ title });
}
```

<Callout type="info" title="Left alone">
A function you wrote and passed to `.default()` keeps its own call. A bare `t.id()` stores `gen_random_uuid()`. `t.id({ default: "uuidv7" })` stores `uuidv7()`, which needs Postgres 18. Pick that for a large write-heavy table: the ids are time-ordered and the index stays local. Random ids are fine elsewhere.
</Callout>


# Overview (/docs/define/connect)

<Anatomy call="connect" shape="args" exported="db">
  <AnatomyPart slot="target" label="Target" code="url," mark="url">
    A connection string, a pool from `open`, or `{ primary, replicas }`. A string or a pool returns the client. A topology returns a promise.
  </AnatomyPart>
  <AnatomyPart slot="schema" label="Schema">
    <AnatomyLine code="{" />
    <AnatomyLine code="schema," mark="required" indent={2} />
    <AnatomyLine code="}," />
    The [schema](/docs/define/schema) types the client. The other decisions are the pages below. [`close()`](#close) is on this page.
  </AnatomyPart>
</Anatomy>

Import `connect` from the [driver](/docs/define/connect/drivers) you installed. The process keeps one client.

<Callout type="info" title="The schema">
The client is typed by the schema you pass. There is no global register.
</Callout>

## The declaration

The list is how often you reach for each part while opening a client. The target and the schema come first.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, { schema: app }); // [!code focus]
```

| Order | Page | You use it for |
| --- | --- | --- |
| 1 | [Drivers](/docs/define/connect/drivers) | The import. |
| 2 | [Target](/docs/define/connect/target) | A URL, a pool, or memory. |
| 3 | [Topology](/docs/define/connect/topology) | A primary and its replicas. |
| 4 | [Check](/docs/define/connect/check) | The server, the floor, and the catalog hash. |
| 5 | [Session](/docs/define/connect/session) | Pool size, TLS, search path, and deadlines. |
| 6 | [Errors](/docs/define/connect/errors) | HTTP statuses and row values. |
| 7 | [Generators](/docs/define/connect/generators) | `uuidv4`, `uuidv7`, and `okid`. |
| 8 | [Observers](/docs/define/connect/observers) | Notices, transactions, and errors. |
| 9 | [OpenTelemetry](/docs/define/connect/otel) | Spans for those observers. |

## Close

A script calls `close()` when it is done. A server keeps the client for the life of the process. The call ends the pool only when this client opened it. Who opened it is [Target](/docs/define/connect/target).

| Client | `close()` |
| --- | --- |
| Opened the pool | Ends that pool. |
| Adopted a pool | Resolves. The pool stays open. |
| `using()` | Absent on that client. |

`await using` is that call at the end of the block. It calls the same `close()`. The client has `[Symbol.asyncDispose]` when the runtime defines that symbol. A runtime without the symbol leaves the client unchanged.

On Bun the adapter keeps `idleTimeout` at 0, which is Bun's default, so a finished script exits without `close()`.

```ts title="script.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export async function read() {
  await using db = connect(process.env.DATABASE_URL!, { schema: app }); // [!code focus]
  await db.notes.find({ limit: 1 });
}
```

A second `close()` waits on the same close when this client opened the pool. The first call starts the close. A later call returns that promise. `for()` and `unscoped()` reuse it. An adopted pool resolves without closing it, on every call.

[`using()`](/docs/define/connect/topology) returns a client with no `close`. The root `close()` stops the probes and closes every pool that `connect` opened.

`close()` before the check settles, on a client that opened the pool, does not reject `connected` when the check succeeds. When the check fails, the rejection is `The client was closed before it connected. Await connected before close(), or do not close it before use.` That promise is [Check](/docs/define/connect/check).


# Observers (/docs/define/connect/observers)

`hookm` is a list of read-only observers. Omitted, no observer runs. Each method is optional. They run in list order. They are events only. They cannot change a query, a commit, or a rollback. An error a hook throws is dropped and never reaches the call. The next observer still runs.

| Method | When |
| --- | --- |
| `onNotice` | A server notice, such as `RAISE NOTICE`. |
| `onTransaction` | A transaction or batch starts, commits, or rolls back. |
| `onError` | An error the runtime could not give to a caller, such as one from an `afterCommit` callback. |

### onNotice

A statement's notices are passed to every `onNotice`.

| Field | Value |
| --- | --- |
| `severity` | A string. |
| `message` | A string. |
| `code` | Optional. A string. |

<Callout type="info" title="Bun.sql">
Bun.sql does not surface `RAISE NOTICE`; see [Drivers](/docs/define/connect/drivers).
</Callout>

### onTransaction

`onTransaction` hears that a transaction or savepoint changed phase. `phase` is `start`, `commit`, or `rollback`. `depth` is 0 for the transaction and 1 and up for savepoints.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

const seen: string[] = [];

export const db = connect(process.env.DATABASE_URL!, {
  schema: app,
  hookm: [
    {
      onTransaction: (event) => {
        seen.push(`${event.phase}:${event.depth}`); // [!code focus]
      },
    },
  ],
});
```

### onError

`error` is `unknown`. An `afterCommit` callback is one case. That error is reported to `onError` and never changes the commit result.

[OpenTelemetry](/docs/define/connect/otel) turns these observers, and `onRoute`, into spans.


# OpenTelemetry (/docs/define/connect/otel)

`okmodel/otel` is experimental. It turns [observers](/docs/define/connect/observers) and [`onRoute`](/docs/define/connect/topology) into OpenTelemetry spans. You pass the tracer. The package does not import `@opentelemetry/*`.

`otel({ tracer })` returns three things.

| Field | Pass it to |
| --- | --- |
| `hookm` | `connect({ hookm })`. Transactions and errors the caller does not receive. |
| `onRoute` | `connect({ onRoute })`. A topology calls it after it chooses an endpoint. |
| `query` | Call it with `inspect()`. That is the fingerprint. |

A string or a pool never calls `onRoute`. Routing spans need `{ primary, replicas }`.

### Attributes

Every span sets `db.system.name` to `postgresql`. The span kind is client.

| Span | Name | Attributes |
| --- | --- | --- |
| Route | `read`, `write`, `tx`, or `lock` | `db.operation.name` is that same word. The endpoint and the reason are a span event named `route`. |
| Transaction | `transaction`, or `savepoint` when the depth is above 0 | `db.operation.name` matches the name. Commit sets status OK. Rollback sets status ERROR and `error.type` to `rollback`. |
| Error | `error` | `error.type` is the error's name. Status is ERROR. |
| `inspect()` | `find notes`, and the same shape for the other reads | `db.operation.name` is the call. `db.collection.name` is the table. `db.query.summary` is the fingerprint. |

`onRoute` fires once, so that span starts and ends in the same call. It has no duration. The route event does not carry a fingerprint, a table, or statement text. `query(inspect())` is what sets `db.query.summary`.

Statement text is off. `statement: true` sets `db.query.text` from that inspection. Parameters are never attributes. A tenant id is not an attribute.

`onError` is an error the caller does not receive, such as one thrown from `afterCommit`. A query that throws to the caller is not that event. The exception is the error name and the OKM code when the error has one, with no message and no stack.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { otel, type OtelSpan, type OtelTracer } from "okmodel/otel";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

const tracer: OtelTracer = {
  startSpan() {
    const span: OtelSpan = {
      setAttribute() {},
      setStatus() {},
      recordException() {},
      addEvent() {},
      end() {},
    };
    return span;
  },
};

const tracing = otel({ tracer });

export const db = await connect(
  {
    primary: process.env.DATABASE_URL!,
    replicas: [process.env.REPLICA_URL!],
  },
  {
    schema: app,
    hookm: [tracing.hookm],
    onRoute: tracing.onRoute,
  },
);

const pending = db.notes.find({ limit: 50 });
tracing.query(await pending.inspect());
await pending;
```

Pass `statement: true` to `otel` when the span should carry `db.query.text`. The parameters still stay off.


# Session (/docs/define/connect/session)

Every connection in the pool uses one session: prepare mode, TLS, a search path, and deadlines. `max` is how many of those connections open.

Set these when this client opens the pool. A pool passed as the target keeps the settings from `open`.

| Field | You set it for |
| --- | --- |
| `max` | How many connections. |
| `prepared` | Named statements, or unnamed. |
| `ssl` | TLS, forwarded to the driver. |
| `searchPath` | The session search path on every connection. |
| `timeouts` | Acquire, statement, transaction, and idle. |
| `timeout`, `signal` | One call, or the first connection check. |

### max

`max` is the number of connections in the pool.

| Value | Result |
| --- | --- |
| omitted | 10 connections on postgres.js, node-postgres, and Bun.sql. PGlite stays one. |
| a number | That many connections. PGlite ignores it. |

Leave `max` out. `connect` forwards `max` only when it is set. postgres.js, node-postgres, and Bun.sql then open 10 connections. PGlite is always one. `connect` does not forward `max` to `open`.

```ts title="pool-size.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

const url = process.env.DATABASE_URL!;

export const db = connect(url, {
  schema: app,
  max: 10, // [!code focus]
});
```

A pool passed as the target keeps the size from `open`. A replica `pool.max` overrides the connect `max` for that endpoint. That field is on [Topology](/docs/define/connect/topology).

### prepared

Named prepared statements are off unless you ask.

| Value | Result |
| --- | --- |
| omitted | Unnamed. It stays valid behind a pooler in transaction mode. |
| `"unnamed"` | The same. |
| `"named"` | Named prepared statements. Not for transaction-mode poolers. |

`okmodel/pg/postgresjs`, `okmodel/pg/pg`, and `okmodel/pg/bun` accept `prepared` on `connect`. `connect` forwards it when that call opens the pool. A value is named only when it is `"named"`. PGlite has no `prepared`. `connect` from `okmodel/pg/pglite` does not forward the field.

Leave `prepared` out, or set `"unnamed"`. postgres.js sets `prepare: false`. A statement with parameters is sent with `{ prepare: false }`. A statement with no parameters is sent with no prepare option. node-postgres sends the query with no `name`. Bun.sql sets `prepare: false`.

`"named"` is the ask. postgres.js sets `prepare: true` on the pool and on each statement. node-postgres sets `name` to `okm_` plus a number, one name per SQL text. Bun.sql sets `prepare: true`.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, {
  schema: app,
  prepared: "named", // [!code focus]
});
```

### TLS

`ssl` is forwarded to the driver. Leave it out and `connect` does not pass it. postgres.js and node-postgres pass the value as `ssl`. Bun.sql passes the value as `tls`. PGlite has no `ssl`.

| Driver | Values |
| --- | --- |
| postgres.js, node-postgres | `true`, `false`, `"require"`, `"allow"`, `"prefer"`, `"verify-full"`, or an object the driver accepts. |
| Bun.sql | `true`, `false`, `"disable"`, `"allow"`, `"prefer"`, `"require"`, `"verify-ca"`, `"verify-full"`. |
| PGlite | Not a field. |

`connect` from `okmodel/pg/postgresjs` copies `ssl` into `open`. `open` passes it as the `ssl` option of `postgres()`. An object is forwarded as-is. `okmodel/pg/pg` does the same for `pg.Pool`. `okmodel/pg/bun` copies `ssl` into `open` and passes that value as the `tls` option of `SQL`. `PgliteConfig` has no `ssl`, so `okmodel/pg/pglite` does not forward it.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, {
  schema: app,
  ssl: "require", // [!code focus]
});
```

### Search path

`searchPath` is the session search path for every connection. When `connect` opens the pool, it passes the string to `open`. Leave `searchPath` out and `open` omits the field. A comma-separated string is passed through as written, with no extra quoting.

| Module | Result |
| --- | --- |
| `okmodel/pg/postgresjs` | `connection.search_path` is the string. |
| `okmodel/pg/pg` | `options` is `-c search_path=` plus the string. |
| `okmodel/pg/bun` | `connection.search_path` is the string. |
| `okmodel/pg/pglite` | `searchPath` is not a field. |

`okmodel/pg/postgresjs` sets `search_path` on the driver's `connection` object. `"app, public"` is that value. The comma and the space stay in the string. `okmodel/pg/pg` interpolates the string into the pool `options` field. The text is `-c search_path=` followed by the value. Interpolation adds no quotes, so `"app, public"` is `-c search_path=app, public`. `okmodel/pg/bun` sets `search_path` on the driver's `connection` object. The comma-separated string is passed through as written. PGlite does not take `searchPath`. `open` reads `dataDir` and `timeouts`. `connect` passes those fields and does not pass a search path.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, {
  schema: app,
  searchPath: "app, public", // [!code focus]
});
```

### Timeouts

Each key is milliseconds. Each one is optional. `connect({ timeouts })` sets the ceilings. With a key omitted, that wait has no deadline. The adapter reads `acquire`. The other keys belong to the runtime.

| Key | What it bounds |
| --- | --- |
| `acquire` | How long to wait for a connection. Past that is OKM1846. |
| `statement` | The timeout of a call that sets none. |
| `transaction` | A whole `tx()`, retries included. |
| `idleInTransaction` | How long a `tx()` waits between statements. |

`statement`, `transaction`, and `idleInTransaction` fail as kind `timeout`. `acquire` fails as OKM1846. That error's kind is `timeout` too.

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, {
  schema: app,
  timeouts: {
    acquire: 2000, // [!code focus]
    statement: 5000, // [!code focus]
    transaction: 10000, // [!code focus]
    idleInTransaction: 5000, // [!code focus]
  },
});
```

#### acquire

The pool waits for a connection up to `timeouts.acquire`. With the key omitted, it waits with no deadline.

<Callout type="error" title="OKM1846">
`Timed out waiting for a connection. This pool did not hand one out within timeouts.acquire.`
</Callout>

#### statement

A call that sets `timeout` uses that number. A call that sets none takes `timeouts.statement`. `batch()` does the same. The code on the error is `timeout`. The message is `The call timed out`.

#### transaction

A `tx()` that sets `timeout` uses that number. A `tx()` that sets none takes `timeouts.transaction`. The deadline is set once, and the retries run inside it. The message is `The transaction timed out.`

A nested `tx()` accepts no options. The outer deadline still bounds it. Passing `timeout` there is OKM1121: `isolation, retry, timeout, and signal belong to the outermost tx(). A nested tx() is a savepoint on the same transaction.`

#### idle

The idle clock runs while no statement is in flight. It starts after `BEGIN`, and again after each statement. With the sample's 5000, the message is `The transaction sat idle for 5000 ms (timeouts.idleInTransaction).` The number is the key.

#### server check

`timeout` and `signal` are fields on `connect`, beside `timeouts`. They bound the check `connected` waits on. That check is [Check](/docs/define/connect/check). It runs the version query. When the catalog hash differs, the same two bound the compatibility read. `timeouts.statement` applies on the next call.

A later call sets its own `timeout`, or takes `timeouts.statement`. On Bun.sql and PGlite a positive `timeout` on this check is OKM1120, because the check is an execute. A `timeout` of 0 fails the check at once as kind `timeout`. An aborted `signal` fails the check as kind `cancelled`. The message is `The call was cancelled`.

#### drivers

A positive `timeout` on a call, on `tx()`, or on `batch()` is refused on Bun.sql and PGlite. Those drivers cannot cancel a statement that is already running. `timeouts.statement` is that number when the call sets none, so a positive `statement` is refused when the call runs. Connect still accepts the object.

postgres.js and node-postgres accept a positive timeout. `timeouts.transaction` and `timeouts.idleInTransaction` stay kind `timeout`, including on Bun.sql and PGlite.

<Callout type="error" title="OKM1120">
The message is `timeout`. The kind is `invalid`. A call or a `batch()` whose timeout is 0 fails at once as kind `timeout`, and the message is `The call timed out`. `signal` is left as the caller set it. This refusal reads only `timeout`.
</Callout>

<Callout type="error" title="OKM1121">
`tx()` requires `timeout` to be a number of milliseconds above 0. Otherwise the message is `timeout must be a number of milliseconds above 0.`
</Callout>


# Target (/docs/define/connect/target)

postgres.js, node-postgres, and Bun.sql take a string or a pool. PGlite takes a data directory, a pool, or `undefined`. The import is [Drivers](/docs/define/connect/drivers). A string means this client opened the pool. An adopted pool stays open. [`close()`](/docs/define/connect#close) is that call.

| Value | Result |
| --- | --- |
| a string, postgres.js, node-postgres, or Bun.sql | Opens a pool. [`close()`](/docs/define/connect#close) closes it. |
| a pool from `open` | Reuses that pool. `close()` does not close it. |
| `{ primary, replicas }` | One pool per endpoint. The call returns a promise. See [Topology](/docs/define/connect/topology). |
| a string, PGlite | A data directory. `connect` returns a promise. `close()` closes it. |
| `undefined`, PGlite | Memory. `open` uses `memory://`. `close()` closes it. |

### string

A string is passed to `open` as `url`. The client owns the pool. [`close()`](/docs/define/connect#close) closes it. node-postgres and Bun.sql do the same. How many connections is [`max`](/docs/define/connect/session#max).

```ts title="db.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = connect(process.env.DATABASE_URL!, { schema: app }); // [!code focus]
```

The URL is not filled in. An empty `DATABASE_URL` is passed through. Check it before you call.

### pool

A pool from `open` is reused. `close()` does not close it. PGlite `open` returns a promise, and `connect` does too. That pool is still reused.

```ts title="pool.ts"
import { connect, open } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

const pool = open({ url: process.env.DATABASE_URL! });

export const db = connect(pool, { schema: app }); // [!code focus]
```

### topology

`{ primary, replicas }` returns a promise. The object is [Topology](/docs/define/connect/topology).

### memory

`undefined` is memory. PGlite has no URL. `dataDir` is omitted, and `open` uses `memory://`. `connect` returns a promise. The client opened that database, so `close()` closes it.

```ts title="memory.ts"
import { connect } from "okmodel/pg/pglite";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = await connect(undefined, { schema: app }); // [!code focus]
```

### directory

A string is the data directory. `connect` returns a promise. `close()` closes it.

```ts title="directory.ts"
import { connect } from "okmodel/pg/pglite";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export const db = await connect("data", { schema: app }); // [!code focus]
```


# Topology (/docs/define/connect/topology)

<Callout type="info" title="Where this fits">
A [target](/docs/define/connect/target) is one endpoint. This page is a primary and its replicas.
</Callout>

`{ primary, replicas }` opens one pool per endpoint. The promise resolves to the client. A string or a pool is one endpoint. It serves either route and has no `using`.

| Field | Default | Result |
| --- | --- | --- |
| `primary` | — | The primary URL. Its name is `primary`. |
| `replicas` | none | A string is a URL with weight 1. An object is `{ url, weight, name, pool }`. |
| `routing.probe` | `"1s"` | How often a replica is probed. A number is milliseconds. |
| `routing.select` | `"weighted"` | `weighted`, `roundRobin`, `leastConnections`, `latencyAware`, or a function. |
| `routing.consistency` | `"session"` | `session` keeps a watermark when replicas are configured. `eventual` leaves it unset. |
| `routing.fallback` | `"primary"` | `error` is OKM1844 when no replica can serve an automatic read. |
| `routing.maxLag` | unset | A duration or a size. A bare number is refused. |
| `onRoute` | unset | Called after an endpoint is chosen. A throw is ignored. The operation still runs. |

`routing` accepts `probe`, `select`, `fallback`, `consistency`, and `maxLag`.

### Primary

`primary` is a non-empty URL. The endpoint is named `primary`.

<Callout type="error" title="OKM1120">
An empty primary is `A topology needs one primary URL. Accepted shape: { primary, replicas }.`
</Callout>

### Replicas

`replicas` is optional. Omit it, or pass `[]`, and the only endpoint is the primary.

A string is a URL with weight 1. An omitted name is `replica-1`, then `replica-2`. An empty name is `replicas[0].name must be a non-empty string.`

```ts title="replicas.ts"
const primary = process.env.DATABASE_URL!;
const east = process.env.REPLICA_URL!;

export const target = {
  primary,
  replicas: [east], // [!code focus]
};
```

An object is `{ url, weight, name, pool }`. `weight`, `name`, and `pool` are optional. `weight` defaults to 1. `pool` is `{ max }`.

A replica `pool.max` overrides [max](/docs/define/connect/session#max) for that endpoint. It is a positive integer: `replicas[0].pool.max must be a positive integer.` The primary uses the connect `max`. A replica that omits `pool` uses it too.

On PGlite a replica url is that endpoint's data directory.

```ts title="topology.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

const primary = process.env.DATABASE_URL!;
const url = process.env.REPLICA_URL!;

export const db = await connect(
  {
    primary,
    replicas: [{ url, weight: 2, name: "east", pool: { max: 4 } }], // [!code focus]
  },
  {
    schema: app,
    routing: { consistency: "session", select: "weighted", maxLag: "5s" },
  },
);
```

A repeated name is refused. A replica named `primary` is `Endpoint name primary is used twice.`

<Callout type="error" title="OKM1120">
A weight of 0 or below is `replicas[0].weight must be a positive number.` The index is that replica's place in the array.
</Callout>

<Callout type="error" title="OKM1120">
An empty string is `replicas[0] is an empty URL.` An object without a url is `replicas[0] needs a url.`
</Callout>

### Probe

Omitted is `"1s"`, 1000 milliseconds. A positive number is milliseconds. A string is trimmed, then a number and `ms`, `s`, or `m`. A fraction is allowed. `"1s"`, `"500ms"`, and `"1m"` are the forms a bad string is shown.

The first probe runs at connect. Later probes wait for that interval. A failed probe or a connection failure adds one. A successful probe clears the count. Two open the circuit, and that replica is left out until a probe succeeds. While the circuit is open the next probe waits longer, up to 30 seconds.

The primary insert position is read on that interval when replicas are configured, consistency is `session` or `maxLag` is set, and that endpoint can read it.

<Callout type="error" title="OKM1120">
A string that is not a duration is `routing.probe must be a number of milliseconds, or "1s", "500ms", or "1m".` A number that is not positive and finite is `routing.probe must be a positive number of milliseconds.` A duration of 0 is `routing.probe must be positive.`
</Callout>

### Select

`routing.select` chooses among replicas the read can use.

| Value | Result |
| --- | --- |
| `"weighted"` | Each eligible replica's running weight grows by its configured weight. The largest wins, then loses the total of the weights that took part. Equal weights take turns in config order. |
| `"roundRobin"` | The next eligible replica after the previous pick, in config order. |
| `"leastConnections"` | The fewest in-flight statements. A tie stays with the earlier replica. |
| `"latencyAware"` | The lowest round-trip. The first sample is kept. Later samples count the new reading at 0.2. A replica with no sample loses to one that has one. A tie stays with the earlier replica. |
| a function | See below. |

#### function

The function runs for an automatic read with two or more eligible replicas. One replica skips the call. `route: "replica"` and `using("replica")` skip it and take the first eligible replica in config order. The built-in names still run for those calls.

Each candidate has `name`, `weight`, `inflight`, `latencyMs`, and `lag`. `lag` is the byte lag, or `null` when either position is unknown. `latencyMs` stays `null` until a probe or a successful read has been timed. The second argument is `{ op: "read" }`. Return one of those candidates, or its name.

```ts title="select.ts"
export const routing = {
  select(
    candidates: readonly {
      readonly name: string;
      readonly weight: number;
      readonly inflight: number;
      readonly latencyMs: number | null;
      readonly lag: number | null;
    }[],
  ) {
    return candidates[0]?.name; // [!code focus]
  },
};
```

<Callout type="error" title="OKM1120">
`routing.select must be "weighted", "roundRobin", "leastConnections", "latencyAware", or a function.` Returning `undefined` is `routing.select returned undefined. It must return a candidate or its name.`
</Callout>

### Consistency

| Value | Result |
| --- | --- |
| `"session"` | After a committed write, the client stores the primary insert position. A replica serves the read when its replay position has reached that value. |
| `"eventual"` | The watermark stays unset. `maxLag` still applies when it is set. |

Before the first committed write, session has no watermark, so that check passes. A rollback leaves the watermark where it was. When a write's insert position cannot be read, and replicas are configured, session treats it as unknown. An automatic read uses the primary, or OKM1844 with `position-unknown`. A replica route is OKM1843. A later primary probe can clear that state.

<Callout type="info" title="Watermark">
One value for the whole `connect()`. The root, `for()`, `unscoped()`, `using()`, and `reserve()` share it.
</Callout>

<Callout type="error" title="OKM1120">
`routing.consistency must be "session" or "eventual". "strong" is not one of those.`
</Callout>

### maxLag

Omitted means no lag ceiling. The value is a string. It is trimmed. A duration is a number and `ms`, `s`, or `m`, such as `5s`, `500ms`, or `2m`. A size is a number and `B`, `KB`, `MB`, or `GB`, such as `16MB`, `512KB`, `1GB`, or `4096B`. `KB` is 1024 bytes. A fraction is allowed.

A duration uses the replica's replay time. A size uses the bytes between the primary insert position and the replica replay position. A caught-up replica has lag zero. A replica whose position is unknown fails the check.

<Callout type="error" title="OKM1120">
A bare number is `routing.maxLag must be a duration ("5s", "500ms", "2m") or a size ("16MB", "512KB", "1GB", "4096B"). 5 is not one of those forms.`
</Callout>

### Fallback

| Value | Result |
| --- | --- |
| `"primary"` | An automatic read uses the primary when no replica can serve it. |
| `"error"` | That read throws OKM1844. |

`route: "replica"` and `using("replica")` have no fallback.

<Callout type="error" title="OKM1844">
`No replica is eligible (behind) and fallback is error.` The word in parentheses is `no-replicas`, `unhealthy`, `position-unknown`, `saturated`, or `behind`.
</Callout>

<Callout type="error" title="OKM1120">
`routing.fallback must be "primary" or "error".`
</Callout>

| Call | Result |
| --- | --- |
| an automatic read | A replica with a closed circuit, within `maxLag`, caught up when session has a watermark, and not full. Otherwise the fallback. |
| `find({ route: "primary" })` | The primary. |
| `find({ route: "replica" })` | A replica with a closed circuit that satisfies the watermark and `maxLag`. A full pool stays eligible. None eligible is OKM1843. |
| `db.using("primary")` or `db.using("replica")` | A client that forces that route. It has no `close` and no `using`. |
| a write, a batch, or a transaction on `using("replica")` | OKM1840, `This operation needs the primary.` |
| a locking `find` with `route: "replica"` | OKM1840, `A locking read needs the primary.` |

### Automatic read

A read with no `route` is automatic. The replica's circuit is closed. Session adds the watermark once one is stored. `maxLag` adds the lag ceiling. A replica with a `max`, no idle connection, size at least that max, and a waiter is skipped. With no `max`, the replica counts as having room.

```ts title="read.ts"
import { db } from "./topology.ts";

export function titles() {
  return db.notes.find({ limit: 20 }); // [!code focus]
}
```

A connection failure tries one other eligible replica. When none is eligible, the primary runs the read unless fallback is `error`.

`no-replicas` means none are configured. `unhealthy` means no replica with a closed circuit can take the read. `behind` means none passed the watermark and `maxLag`. `saturated` means each remaining replica is full. `position-unknown` means session could not read the insert position.

### Primary route

`route: "primary"` reads the primary.

```ts title="primary-route.ts"
import { db } from "./topology.ts";

export function titles() {
  return db.notes.find({ limit: 20, route: "primary" }); // [!code focus]
}
```

### Replica route

`route: "replica"` reads a replica with a closed circuit that satisfies the watermark and `maxLag`. A replica at its max stays eligible. The call has no fallback.

```ts title="replica-route.ts"
import { db } from "./topology.ts";

export function titles() {
  return db.notes.find({ limit: 20, route: "replica" }); // [!code focus]
}
```

A connection failure can try another eligible replica. The retry stays on a replica.

<Callout type="error" title="OKM1843">
`No replica is eligible. This call does not read the primary.`
</Callout>

### using

`db.using("primary")` or `db.using("replica")` returns a client that forces that route. It has no `close` and no `using`. The root `close()` still closes the pools.

```ts title="using.ts"
import { db } from "./topology.ts";

export function writer() {
  return db.using("primary"); // [!code focus]
}

export function reader() {
  return db.using("replica"); // [!code focus]
}
```

A write, a batch, and a transaction run on the primary. On `using("replica")` each of those is OKM1840. A locking `find` with `route: "replica"`, including on that client, is the same code and the other message.

<Callout type="error" title="OKM1840">
A write, a batch, or a transaction on a replica route is `This operation needs the primary.` A locking find is `A locking read needs the primary.`
</Callout>

<Callout type="error" title="OKM1120">
`using takes "primary" or "replica".`
</Callout>

<Callout type="error" title="OKM1120">
`Table "using" collides with client.using(). Rename the table.`
</Callout>

### onRoute

`onRoute` is called after an endpoint is chosen, with the operation (`read`, `write`, `tx`, or `lock`), the endpoint name, and the reason. A throw is ignored. The operation still runs.

```ts title="on-route.ts"
const seen: string[] = [];

export function onRoute(event: {
  readonly op: "read" | "write" | "tx" | "lock";
  readonly endpoint: string;
  readonly reason: string;
}): void {
  seen.push(event.endpoint); // [!code focus]
}
```

### Inspect

On this client, `inspect().routing.reason` is the same string `onRoute` receives. The preview does not call `onRoute` and does not move which replica is next. A string or a pool stays `single-endpoint`, and that call is synchronous.

```ts title="inspect-route.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", { id: t.identity(), title: t.text() });
const app = schema({ tables: [notes] });

export async function reason(primary: string, replica: string) {
  const db = await connect(
    { primary, replicas: [{ url: replica, name: "east" }] },
    { schema: app },
  );
  const seen = await db.notes.find({ limit: 1 }).inspect();
  return seen.routing.reason; // [!code focus]
}
```

### Close

`close()` on the root stops the probes and closes the pools this call opened. A client from `using()` has no `close`. A failed connect closes the pools it had opened. That call is [`close()`](/docs/define/connect#close).


# Casing (/docs/define/schema/casing)

`casing` is the SQL spelling of a name you did not set. Insert, update, and `where` still use the field name. The client name stays the name you passed to [`table()`](/docs/define/table).

| Value | Result |
| --- | --- |
| omitted | The SQL name is the field name, and the table name. |
| `"snake"` | An uppercase letter inserts `_`, then the letter is lowercased. |
| any other value | Refused. |

### omitted

Leave `casing` out and Postgres stores the name you wrote.

```ts title="omitted.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  publishedAt: t.timestamptz().nullable(), // [!code focus]
});

export default schema({ tables: [notes] });
```

### snake

`"snake"` rewrites a camelCase table name and a camelCase field. The first character never gains a leading `_`. `publishedAt` is `published_at`. `userID` is `user_i_d`. `PublishedAt` is `published_at`. A digit does not insert `_`, so `iso8601Date` is `iso8601_date`. An uppercase letter that already follows `_` does not add a second one: `user_ID` is `user_i_d`.

[sqlName](/docs/define/table/names#sql-name) and `renamedFrom` are stored as you wrote them. Indexes, checks, and foreign keys then use that SQL name.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("publishedNotes", {
  id: t.identity(),
  publishedAt: t.timestamptz().nullable(), // [!code focus]
  headline: t.text().sqlName("title"), // [!code focus]
});

export default schema({
  tables: [notes],
  casing: "snake", // [!code focus]
});
```

<Callout type="info" title="Stored names">
The table is `published_notes`. `publishedAt` is `published_at`. `headline` is `title`, because `sqlName` wins.
</Callout>

<Callout type="error" title="OKM1060">
Anything other than `"snake"` is `schema() casing camel must be snake.`
</Callout>


# Several catalogs (/docs/define/schema/catalogs)

`schema()` compiles `public`. The Postgres schema comes from `defineConfig({ namespace })`. Each catalog has its own namespace, its own ownership, and its own `okm_meta`.

`external(users)` marks a table the other catalog exported. The plan does not create, alter, or drop it. A managed table in the same database may reference it. A key that points at another database is OKM1842.

`okm catalog export` writes the column snapshot (names, types, nullability). The dependent lists that file in `external`. `okm check` and `okm migrate check` compare the declaration and the database with the file. A stale or missing snapshot is OKM1841. `okm check` lists a cross-catalog foreign key as `coupling`. The full catalog JSON is [`describe(schema)`](/docs/reference/schema-api), not the export.

`connect()` reads `okm_meta` in that namespace. `okm build` writes `.okm/namespace` when the name is not `public`. A missing table still skips the check. A hash that does not match is OKM1520.

### owning catalog

```ts title="identity/schema.ts"
import { schema, table, t } from "okmodel/pg";

export const users = table("users", {
  id: t.uuid().primaryKey(),
  email: t.text(),
});

export default schema({ tables: [users] });
```

```ts title="identity/okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL,
  namespace: "identity",
});
```

```sh
okm generate init
okm migrate apply
okm catalog export --out identity.okm.json
```

### dependent catalog

```ts title="billing/schema.ts"
import { external, schema, table, t } from "okmodel/pg";
import { users } from "../identity/schema.ts";

const invoices = table("invoices", {
  id: t.uuid().primaryKey(),
  userId: t.uuid().references("users"),
});

export default schema({ tables: [external(users), invoices] });
```

```ts title="billing/okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL,
  namespace: "billing",
  external: ["../identity/identity.okm.json"],
});
```

```sh
okm generate init
okm migrate apply
okm check
okm migrate check
```

The invoices migration references `"identity"."users"`. It does not create that table. Both commands pass while the snapshot matches the database. Drop `email` in the identity catalog and skip `okm catalog export`: the billing check fails with OKM1841.

<Callout type="info" title="Snapshot">
The file has `catalogHash` and, separately, `logical` (`namespace`, `name`, `format`). It has column names and types. It has no function and no connection URL.
</Callout>

<Callout type="error" title="OKM1842">
`A foreign key references identity in database <name>. This database is <other>.` Keep the foreign key inside one database.
</Callout>


# Codecs (/docs/define/schema/codecs)

`codecs` chooses the TypeScript value of `t.bigint()`, `t.identity()`, and `t.numeric()`. The SQL type stays `bigint` or `numeric`. A time column is already `Temporal`. Omit a key and that codec stays at its default.

The column `as` is what types the value. While the schema codec is `"string"`, a column may pass another `as`. When the schema codec is `"number"` or `"bigint"`, every column of that type must pass the same `as`. A bare column was built as `"string"`.

| Codec | Values | Default |
| --- | --- | --- |
| `bigint` | `"string"`, `"number"`, `"bigint"` | `"string"` |
| `numeric` | `"string"`, `"number"` | `"string"` |
| `timestamps` | `"temporal"` | `"temporal"` |

### bigint

`t.bigint()` and `t.identity()` share this codec. `"number"` is a safe integer. A value outside that range is OKM1210. See [bigint](/docs/define/table/columns#bigint).

#### string

Omitted and `"string"` are a decimal string. The SQL value stays exact past a safe integer.

```ts title="bigint-string.ts"
import { schema, table, t } from "okmodel/pg";

const payments = table("payments", {
  id: t.identity(),
  cents: t.bigint(), // [!code focus]
});

export default schema({ tables: [payments] });
```

#### number

`"number"` is a safe integer. The column passes the same `as`.

```ts title="bigint-number.ts"
import { schema, table, t } from "okmodel/pg";

const payments = table("payments", {
  id: t.identity({ as: "number" }), // [!code focus]
  cents: t.bigint({ as: "number" }), // [!code focus]
});

export default schema({
  tables: [payments],
  codecs: { bigint: "number" }, // [!code focus]
});
```

#### native

`"bigint"` is a JavaScript bigint.

```ts title="bigint-native.ts"
import { schema, table, t } from "okmodel/pg";

const payments = table("payments", {
  id: t.identity({ as: "bigint" }),
  cents: t.bigint({ as: "bigint" }), // [!code focus]
});

export default schema({
  tables: [payments],
  codecs: { bigint: "bigint" }, // [!code focus]
});
```

<Callout type="error" title="OKM1060">
A bare column under `"number"` or `"bigint"` is `Schema codecs.bigint is number, and payments.cents was built as string. Set as: "number" on that column.` Any other schema value is `schema() codecs.bigint date must be string, number, or bigint.`
</Callout>

### numeric

`t.numeric()` is exact. `{ as: "number" }` can lose digits past a JavaScript number. See [numeric](/docs/define/table/columns#numbers).

#### string

Omitted and `"string"` keep every digit.

```ts title="numeric-string.ts"
import { schema, table, t } from "okmodel/pg";

const payments = table("payments", {
  id: t.identity(),
  amount: t.numeric(12, 2), // [!code focus]
});

export default schema({ tables: [payments] });
```

#### number

`"number"` needs `{ as: "number" }` on the column. Precision and scale stay the SQL type.

```ts title="numeric-number.ts"
import { schema, table, t } from "okmodel/pg";

const payments = table("payments", {
  id: t.identity(),
  amount: t.numeric(12, 2, { as: "number" }), // [!code focus]
});

export default schema({
  tables: [payments],
  codecs: { numeric: "number" }, // [!code focus]
});
```

<Callout type="error" title="OKM1060">
A bare column under `"number"` is `Schema codecs.numeric is number, and payments.amount was built as string. Set as: "number" on that column.` Any other schema value is `schema() codecs.numeric bigint must be string or number.`
</Callout>

### timestamps

`"temporal"` is the only value. Omitting it is the same. A time column has no `as`. `t.timestamptz()` is `Temporal.Instant`. See [time](/docs/define/table/columns#time).

```ts title="timestamps.ts"
import { schema, table, t } from "okmodel/pg";

const payments = table("payments", {
  id: t.identity(),
  paidAt: t.timestamptz(), // [!code focus]
});

export default schema({
  tables: [payments],
  codecs: { timestamps: "temporal" }, // [!code focus]
});
```

<Callout type="error" title="OKM1060">
Any other value is `schema() codecs.timestamps date must be temporal.`
</Callout>

### together

One schema can set every codec. Each bigint column matches `"bigint"`. The numeric column matches `"number"`.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const payments = table("payments", {
  id: t.identity({ as: "bigint" }),
  cents: t.bigint({ as: "bigint" }), // [!code focus]
  amount: t.numeric(12, 2, { as: "number" }), // [!code focus]
  paidAt: t.timestamptz(),
});

export default schema({
  tables: [payments],
  codecs: {
    bigint: "bigint", // [!code focus]
    numeric: "number", // [!code focus]
    timestamps: "temporal", // [!code focus]
  },
});
```


# Id default (/docs/define/schema/defaults)

`defaults` has one field, `id`. It replaces a bare `t.id()`. A column that passes `default` is left alone. `"none"` is that column option. It is not a schema value.

The id is omitted from insert for a database default and for a client generator. `"none"` is required on insert and still omitted from update.

### omitted

Omitted and `{}` store `gen_random_uuid()`. `"uuidv4"` stores the same function. It is built in from Postgres 13, so a bare `t.id()` works on every supported major. Random ids are fine for most tables. A large write-heavy table can pick `uuidv7`: the ids are time-ordered and the index stays local. That function needs Postgres 18.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.id(), // [!code focus]
  title: t.text(),
});

export default schema({
  tables: [notes],
});
```

### uuidv7

`"uuidv7"` stores `uuidv7()`. Keep the previous default with `defaults: { id: "uuidv7" }`.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.id(),
  title: t.text(),
});

export default schema({
  tables: [notes],
  defaults: { id: "uuidv7" }, // [!code focus]
});
```

<Callout type="error" title="OKM1812">
`uuidv7()` needs PostgreSQL 18 or higher. A [minPostgres](/docs/define/schema/min-postgres) floor below 18 is `uuidv7() needs PostgreSQL 18 or higher. minPostgres: 18.` An omitted floor does not name a version, so an explicit `uuidv7()` stays until apply.
</Callout>

### uuidv4

`"uuidv4"` stores `gen_random_uuid()`, the same expression a bare `t.id()` stores. That function is built in from Postgres 13.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.id(),
  title: t.text(),
});

export default schema({
  tables: [notes],
  defaults: { id: "uuidv4" }, // [!code focus]
});
```

### client

A generator from `okmodel/ids` runs on insert. The catalog stores no default. Changing an OKID prefix, length, or sortable flag does not change the catalog hash. The SQL type does: `okid()` is `text`, and `uuidv4` or `uuidv7` stay `uuid`.

#### uuid

`uuidv4` and `uuidv7` stay `uuid`. The client `uuidv7` mints a time-ordered id, so a server older than Postgres 18 can still use one.

```ts title="schema.ts"
import { uuidv7 } from "okmodel/ids";
import { schema, table, t } from "okmodel/pg";

const sessions = table("sessions", {
  id: t.id(),
  name: t.text(),
});

export default schema({
  tables: [sessions],
  defaults: { id: uuidv7 }, // [!code focus]
});
```

#### okid

`okid()` is `text` with collation `C`. Calling it returns the generator. It does not mint the id here. Length, prefix, and sortable are on [OKID](/docs/define/table/columns#okid).

```ts title="schema.ts"
import { okid } from "okmodel/ids";
import { schema, table, t } from "okmodel/pg";

const users = table("users", {
  id: t.id(),
  name: t.text(),
});

export default schema({
  tables: [users],
  defaults: { id: okid({ prefix: "usr_" }) }, // [!code focus]
});
```

### column

A column option wins. A bare `t.id()` takes the schema default. `t.id({ default: "uuidv4" })` stays `gen_random_uuid()` and stays `uuid`.

```ts title="schema.ts"
import { okid } from "okmodel/ids";
import { schema, table, t } from "okmodel/pg";

const users = table("users", {
  id: t.id(), // [!code focus]
  name: t.text(),
});

const sessions = table("sessions", {
  id: t.id({ default: "uuidv4" }), // [!code focus]
  name: t.text(),
});

export default schema({
  tables: [users, sessions],
  defaults: { id: okid({ prefix: "usr_" }) },
});
```

<Callout type="warning" title="OKM1060">
Anything other than an object is `schema() defaults must be an object.` Another key is `schema() defaults.createdAt is not a schema default. Accepted names: id.` Another value, including `"none"`, is `schema() defaults.id must be "uuidv7", "uuidv4", or a client generator.`
</Callout>


# Extensions (/docs/define/schema/extensions)

`extensions` is a list. Each value contributes its catalog record when the catalog is first read. `schema()` does not install the extension. Migrate writes `create extension` before the table that needs it. An omitted list, an empty list, and a value that is not a list add no extension record.

`citext()`, `pgTrgm()`, and `extension("name")` take the same options.

### citext

`citext()` from `okmodel/pg/citext` declares `citext`. A [citext](/docs/define/table/columns#citext) column needs that name.

```ts title="citext.ts"
import { citext } from "okmodel/pg/citext";
import { schema, table, t } from "okmodel/pg";

const people = table("people", {
  id: t.identity(),
  email: t.citext(), // [!code focus]
});

export default schema({
  tables: [people],
  extensions: [citext()], // [!code focus]
});
```

### trigram

`pgTrgm()` from `okmodel/pg/pg_trgm` declares `pg_trgm`. `similar` and `wordSimilar` compare a string. `gin` and `gist` build an index on a SQL column name.

#### where

`similar` is `column % query`. `wordSimilar` is `column <% query`. Both take the text to compare.

```ts title="where.ts"
import { connect } from "okmodel/pg/postgresjs";
import { pgTrgm } from "okmodel/pg/pg_trgm";
import { schema, table, t } from "okmodel/pg";

const trgm = pgTrgm();

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({
  tables: [notes],
  extensions: [trgm], // [!code focus]
});

export async function search(url: string, title: string) {
  await using db = connect(url, { schema: app });
  return db.notes.find({
    where: { title: trgm.similar(title) }, // [!code focus]
    limit: 1,
  });
}
```

#### index

`gin` uses `gin_trgm_ops`. `gist` uses `gist_trgm_ops`. The argument is the SQL column name.

```ts title="index.ts"
import { pgTrgm } from "okmodel/pg/pg_trgm";
import { schema, table, t } from "okmodel/pg";

const trgm = pgTrgm();

const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  { indexes: (column) => [trgm.gin(column.title.name)] }, // [!code focus]
);

export default schema({
  tables: [notes],
  extensions: [trgm],
});
```

### unaccent

`unaccent()` from `okmodel/pg/unaccent` declares `unaccent`. `unaccent(value)` is `unaccent(column) = unaccent(value)`.

```ts title="unaccent.ts"
import { connect } from "okmodel/pg/postgresjs";
import { unaccent } from "okmodel/pg/unaccent";
import { schema, table, t } from "okmodel/pg";

const fold = unaccent();

const words = table("words", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({
  tables: [words],
  extensions: [fold],
});

export async function folded(url: string) {
  await using db = connect(url, { schema: app });
  return db.words.find({
    where: { title: fold.unaccent("Café") }, // [!code focus]
    limit: 1,
  });
}
```

### hstore

`hstore()` from `okmodel/pg/hstore` declares `hstore`. `hasKey` is `column ? key`. `gin` uses `gin_hstore_ops`. `gist` uses `gist_hstore_ops`.

```ts title="hstore.ts"
import { connect } from "okmodel/pg/postgresjs";
import { hstore } from "okmodel/pg/hstore";
import { custom, schema, table, t } from "okmodel/pg";

const store = hstore();

const docs = table("docs", {
  id: t.identity(),
  attrs: custom<string>({
    sqlType: "hstore",
    encode: (value) => value,
    decode: (wire) => wire,
    extension: "hstore",
  }),
});

const app = schema({
  tables: [docs],
  extensions: [store],
});

export async function withKey(url: string) {
  await using db = connect(url, { schema: app });
  return db.docs.find({
    where: { attrs: store.hasKey("a") }, // [!code focus]
    limit: 1,
  });
}
```

### ltree

`ltree()` from `okmodel/pg/ltree` declares `ltree`. `ancestorOf` is `column @> path`. `descendantOf` is `column <@ path`. `gist` uses `gist_ltree_ops`. This is not the parent-column walk.

```ts title="ltree.ts"
import { connect } from "okmodel/pg/postgresjs";
import { ltree } from "okmodel/pg/ltree";
import { schema, table, t } from "okmodel/pg";

const tree = ltree();

const places = table("places", {
  id: t.identity(),
  path: t.ltree(),
});

const app = schema({
  tables: [places],
  extensions: [tree],
});

export async function parents(url: string) {
  await using db = connect(url, { schema: app });
  return db.places.find({
    where: { path: tree.ancestorOf("Top.Science.Physics") }, // [!code focus]
    limit: 1,
  });
}
```

### btree_gist and btree_gin

`btreeGist()` from `okmodel/pg/btree_gist` indexes an integer with `gist_int4_ops`. `btreeGin()` from `okmodel/pg/btree_gin` indexes an integer with `int4_ops`.

```ts title="btree.ts"
import { btreeGin } from "okmodel/pg/btree_gin";
import { btreeGist } from "okmodel/pg/btree_gist";
import { schema, table, t } from "okmodel/pg";

const gist = btreeGist();
const gin = btreeGin();

const nums = table(
  "nums",
  {
    id: t.identity(),
    n: t.integer(),
    m: t.integer(),
  },
  { indexes: (column) => [gist.gist(column.n.name), gin.gin(column.m.name)] }, // [!code focus]
);

export default schema({
  tables: [nums],
  extensions: [gist, gin],
});
```

### fuzzystrmatch

`fuzzystrmatch()` from `okmodel/pg/fuzzystrmatch` declares `fuzzystrmatch`. `levenshtein(value, distance)` is `levenshtein(column, value) = distance`. `daitchMokotoff` needs extension 1.2 and PostgreSQL 16 (OKM1812).

```ts title="fuzzy.ts"
import { connect } from "okmodel/pg/postgresjs";
import { fuzzystrmatch } from "okmodel/pg/fuzzystrmatch";
import { schema, table, t } from "okmodel/pg";

const fuzzy = fuzzystrmatch();

const words = table("words", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({
  tables: [words],
  extensions: [fuzzy],
});

export async function close(url: string) {
  await using db = connect(url, { schema: app });
  return db.words.find({
    where: { title: fuzzy.levenshtein("sitting", 3) }, // [!code focus]
    limit: 1,
  });
}
```

### pgcrypto

`pgcrypto()` from `okmodel/pg/pgcrypto` declares `pgcrypto`. `digest(algorithm, hex)` compares `encode(digest(column, algorithm), 'hex')` with that hex. `fipsMode` needs extension 1.4 and PostgreSQL 18 (OKM1812).

```ts title="digest.ts"
import { connect } from "okmodel/pg/postgresjs";
import { pgcrypto } from "okmodel/pg/pgcrypto";
import { schema, table, t } from "okmodel/pg";

const crypto = pgcrypto();

const words = table("words", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({
  tables: [words],
  extensions: [crypto],
});

export async function hashed(url: string) {
  await using db = connect(url, { schema: app });
  return db.words.find({
    where: {
      title: crypto.digest(
        "sha256",
        "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
      ),
    }, // [!code focus]
    limit: 1,
  });
}
```

### name

`extension("name")` from `okmodel/ext` declares that name. There is no typed operator. `uuid-ossp` is this form. A [ltree](/docs/define/table/columns#search) column can use `ltree()` from `okmodel/pg/ltree` instead.

```ts title="name.ts"
import { extension } from "okmodel/ext";
import { schema, table, t } from "okmodel/pg";

const keys = table("keys", {
  id: t.identity(),
});

export default schema({
  tables: [keys],
  extensions: [extension("uuid-ossp")], // [!code focus]
});
```

<Callout type="error" title="OKM1020">
A `citext` or `ltree` column, when this list is omitted or empty, is `column email depends on extension citext, which is not in the catalog.` The name in that sentence is the column, then the extension.
</Callout>

<Callout type="error" title="OKM1810">
A list that declares a different extension is `Extension citext is used but not declared.` A value in the list that is not an extension is `schema({ extensions }) must be a list of extensions.`
</Callout>

<Callout type="error" title="OKM1813">
The same name twice is `Extension citext is defined twice.`
</Callout>

### options

Every call takes `version`, `schema`, and `relocatable`. Omitted, the version is unpinned, the schema is `public`, and `relocatable` is `true`. An unpinned record matches any installed version. Schema and `relocatable` still have to match.

#### version

`1.6` is an exact pin. Migrate writes `version '1.6'`. `>=1.6` is a floor. It is stored and is not written. A shorter tail compares as zero.

```ts title="version.ts"
import { citext } from "okmodel/pg/citext";
import { schema, table, t } from "okmodel/pg";

const people = table("people", {
  id: t.identity(),
  email: t.citext(),
});

export default schema({
  tables: [people],
  extensions: [citext({ version: "1.6" })], // [!code focus]
});
```

A floor asks for that version or newer and does not pin `create extension`.

```ts title="floor.ts"
import { citext } from "okmodel/pg/citext";
import { schema, table, t } from "okmodel/pg";

const people = table("people", {
  id: t.identity(),
  email: t.citext(),
});

export default schema({
  tables: [people],
  extensions: [citext({ version: ">=1.6" })], // [!code focus]
});
```

#### schema

`schema` is where the extension's objects are installed. The default is `public`.

```ts title="schema-name.ts"
import { extension } from "okmodel/ext";
import { schema, table, t } from "okmodel/pg";

const places = table("places", {
  id: t.identity(),
  path: t.ltree(),
});

export default schema({
  tables: [places],
  extensions: [extension("ltree", { schema: "extensions" })], // [!code focus]
});
```

#### relocatable

`relocatable: false` refuses a later move. The default `true` allows `alter extension … set schema`.

```ts title="relocatable.ts"
import { citext } from "okmodel/pg/citext";
import { schema, table, t } from "okmodel/pg";

const people = table("people", {
  id: t.identity(),
  email: t.citext(),
});

export default schema({
  tables: [people],
  extensions: [citext({ relocatable: false })], // [!code focus]
});
```

<Callout type="error" title="OKM1020">
A version that is neither a dotted pin nor a `>=` floor is `Extension version latest must be a dotted version or a >= floor.`
</Callout>

<Callout type="error" title="OKM1811">
An extension the server does not have is `Extension citext is not available on the server. Nothing was changed.`
</Callout>

<Callout type="error" title="OKM1814">
A move of a non-relocatable extension is `Extension citext is not relocatable, so it cannot move to extensions.` A lower pin is `Extension citext cannot move from 1.6 to 1.5. Postgres cannot lower an extension version.` No update path is `Extension citext has no update path from 1.4 to 1.6. Nothing was changed.` Dependents still using it are `Extension citext still has dependents. Nothing was dropped.`
</Callout>

### together

`citext` for the column, `pg_trgm` for the index, and `ltree` by name.

```ts title="together.ts"
import { extension } from "okmodel/ext";
import { citext } from "okmodel/pg/citext";
import { pgTrgm } from "okmodel/pg/pg_trgm";
import { schema, table, t } from "okmodel/pg";

const trgm = pgTrgm({ version: "1.6" });

const people = table(
  "people",
  {
    id: t.identity(),
    email: t.citext(),
    title: t.text(),
    path: t.ltree(),
  },
  { indexes: (column) => [trgm.gin(column.title.name)] },
);

export default schema({
  tables: [people],
  extensions: [citext(), trgm, extension("ltree")], // [!code focus]
});
```

### Create your own

`citext()` and `pgTrgm()` are the built-ins above. A name they do not cover is `extension()` from `okmodel/ext`. That call has the same options and no typed operator.

#### Simple

`unaccent` is one name. `okm generate` writes `create extension "unaccent" schema "public"` before the table.

```ts title="own.ts"
import { extension } from "okmodel/ext";
import { schema, table, t } from "okmodel/pg";

const documents = table("documents", {
  id: t.identity(),
  body: t.text(),
});

export default schema({
  tables: [documents],
  extensions: [extension("unaccent")], // [!code focus]
});
```

#### Advanced

`version` is an exact pin and is written. `schema` is the install schema. It has to exist already: `okm migrate apply` does not create it. Run `create schema "extensions"` on the database before apply. The type then lives in that schema, not on the search path, so the column names `extensions.hstore`. `relocatable: false` refuses a later move. `t.custom({ extension })` records that the column needs this name, so the list has to declare it.

`okm generate` writes `create extension "hstore" schema "extensions" version '1.8'` before the table.

```ts title="own-advanced.ts"
import { extension } from "okmodel/ext";
import { schema, table, t } from "okmodel/pg";

const places = table("places", {
  id: t.identity(),
  attrs: t.custom({
    sqlType: "extensions.hstore",
    extension: "hstore", // [!code focus]
    encode: (value: string) => value,
    decode: (wire: string) => wire,
  }),
});

export default schema({
  tables: [places],
  extensions: [
    extension("hstore", {
      version: "1.8", // [!code focus]
      schema: "extensions", // [!code focus]
      relocatable: false, // [!code focus]
    }),
  ],
});
```

<Callout type="error" title="OKM1814">
A later move of that extension is `Extension hstore is not relocatable, so it cannot move to public.`
</Callout>


# Functions (/docs/define/schema/functions)

`functions` is a list of `fn()` from `okmodel/fn`. The catalog record is built when the catalog is first read. `schema()` does not run the body. Overloads differ by argument types. The same name with the same argument types is one function.

`returns`, `language`, and `body` are required. `language` is `"sql"` or `"plpgsql"`. Volatility defaults to `"volatile"`. Security defaults to `"invoker"`. The install schema defaults to `public`.

The value `fn()` returns is callable. `slugify(ident("title"))` is a `where` operand. `slugify(ident("title")).as("slug")` is a `select` entry. A numeric return is a number. A boolean return is read as text, as is any other non-numeric return. A string, number, boolean, or null is a parameter. `ident` names a field. `orderBy` does not accept the call.

### sql

`"sql"` stores the body as written. `dependsOn` is optional.

```ts title="sql.ts"
import { fn } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const answer = fn("answer", {
  returns: "integer",
  language: "sql",
  body: sql`select 1`, // [!code focus]
});

export default schema({
  tables: [notes],
  functions: [answer], // [!code focus]
});
```

#### atomic

A body that begins with `BEGIN ATOMIC` is marked atomic. Its edges are read from `pg_depend`. It does not need `dependsOn`. The catalog keeps the text you wrote. `okm migrate check` and `okm check` reprint that body on the connected server before they compare, so a qualifier Postgres adds, such as `by_id.n` for an argument used beside a table, is not a difference (D273). Two overloads of one name are matched by that name and the identity arguments, so each keeps its own reprinted body.

```ts title="atomic.ts"
import { fn } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const answer = fn("answer", {
  returns: "integer",
  language: "sql",
  body: sql`begin atomic select 1`, // [!code focus]
});

export default schema({
  tables: [notes],
  functions: [answer],
});
```

### plpgsql

`"plpgsql"` needs `dependsOn`. Postgres does not record dependencies inside that body. A table is an object dependency. `{ table, column }` is a column dependency. `column` is the SQL name. With [casing](/docs/define/schema/casing) `"snake"`, `updatedAt` is `updated_at`.

```ts title="plpgsql.ts"
import { fn } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
  updatedAt: t.timestamptz().defaultSql("now()"),
});

const touch = fn("touch_notes", {
  returns: "trigger",
  language: "plpgsql",
  body: sql`begin new."updatedAt" = now(); return new; end`,
  dependsOn: [{ table: notes, column: "updatedAt" }], // [!code focus]
});

export default schema({
  tables: [notes],
  functions: [touch],
});
```

<Callout type="error" title="OKM1824">
plpgsql without `dependsOn` is `Function touch_notes is plpgsql and has no dependsOn. Postgres does not record dependencies inside the body.`
</Callout>

<Callout type="error" title="OKM1020">
A missing column is `dependsOn names column updated_at, which notes does not have.` A table that is not in this schema is `Table notes is not in the catalog.` An empty body is `Function touch_notes needs a body.` `BEGIN ATOMIC` with plpgsql is `Function touch_notes BEGIN ATOMIC needs language sql.` Another language is `Function touch_notes language must be sql or plpgsql.`
</Callout>

<Callout type="error" title="OKM1023">
The same name and the same argument types twice is `Duplicate identity for function touch_notes.`
</Callout>

### arguments

Each argument has a `name` and a `type`. Names live in the definition. Argument types are the identity.

```ts title="arguments.ts"
import { fn } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const lengthOf = fn("length_of", {
  arguments: [{ name: "title", type: "text" }], // [!code focus]
  returns: "integer",
  language: "sql",
  body: sql`select char_length(title)`,
});

export default schema({
  tables: [notes],
  functions: [lengthOf],
});
```

### volatility

`"volatile"`, `"stable"`, or `"immutable"`. Omitted is `"volatile"`.

```ts title="volatility.ts"
import { fn } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const answer = fn("answer", {
  returns: "integer",
  language: "sql",
  volatility: "immutable", // [!code focus]
  body: sql`select 1`,
});

export default schema({
  tables: [notes],
  functions: [answer],
});
```

<Callout type="error" title="OKM1020">
Another value is `Function answer volatility literal is not supported.`
</Callout>

### security

`"invoker"` or `"definer"`. Omitted is `"invoker"`. `"definer"` needs `searchPath`. Each schema in that list, split on commas, is an identifier.

```ts title="security.ts"
import { fn } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const answer = fn("answer", {
  returns: "integer",
  language: "sql",
  security: "definer", // [!code focus]
  searchPath: "public", // [!code focus]
  body: sql`select 1`,
});

export default schema({
  tables: [notes],
  functions: [answer],
});
```

<Callout type="error" title="OKM1823">
Definer with no `searchPath`, or one that is only spaces, is `Function answer is security definer and has no search_path.`
</Callout>

<Callout type="error" title="OKM1020">
An empty `searchPath` on an invoker is `Function answer search_path is empty.` Another security value is `Function answer security owner is not supported.`
</Callout>

### install schema

`schema` on `fn()` is the install schema. The default is `public`.

```ts title="install.ts"
import { fn } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const answer = fn("answer", {
  returns: "integer",
  language: "sql",
  schema: "app", // [!code focus]
  body: sql`select 1`,
});

export default schema({
  tables: [notes],
  functions: [answer],
});
```

### together

A definer, arguments, a column dependency, and a [trigger](/docs/define/schema/triggers) that calls it. [Casing](/docs/define/schema/casing) `"snake"` makes the column `updated_at`.

```ts title="together.ts"
import { fn, trigger } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
  updatedAt: t.timestamptz().defaultSql("now()"),
});

const touch = fn("touch_notes", {
  arguments: [{ name: "extra", type: "text" }],
  returns: "trigger",
  language: "plpgsql",
  security: "definer",
  searchPath: "public",
  body: sql`begin new.updated_at = now(); return new; end`,
  dependsOn: [{ table: notes, column: "updated_at" }],
});

export default schema({
  tables: [notes],
  casing: "snake",
  functions: [touch], // [!code focus]
  triggers: [
    trigger("notes_touch", {
      on: notes,
      timing: "before",
      events: ["update"],
      level: "row",
      calls: touch,
    }),
  ],
});
```

### Create your own

There is no built-in function. `fn()` from `okmodel/fn` is the declaration. The sections above are its options.

#### Simple

One SQL function, no arguments. `okm generate` writes it after the table: `create function "public"."answer"() returns integer language sql volatile security invoker as $okm$select 1$okm$`.

```ts title="own.ts"
import { fn } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const answer = fn("answer", {
  returns: "integer",
  language: "sql",
  body: sql`select 1`, // [!code focus]
});

export default schema({
  tables: [notes],
  functions: [answer], // [!code focus]
});
```

#### Advanced

`slugify` is immutable and takes one argument. `touch_notes` is plpgsql, so it lists the column it writes, and it is a definer, so it sets `searchPath`. The trigger is created after both functions. `okm generate` writes the table, then `slugify`, then `touch_notes`, then `create trigger "notes_touch" before update of "title" on "public"."notes" for each row when (old.title is distinct from new.title) execute function "public"."touch_notes"()`.

```ts title="own-advanced.ts"
import { fn, trigger } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
  updatedAt: t.timestamptz().defaultSql("now()"),
});

const slugify = fn("slugify", {
  arguments: [{ name: "title", type: "text" }], // [!code focus]
  returns: "text",
  language: "sql",
  volatility: "immutable", // [!code focus]
  body: sql`select lower(title)`,
});

const touch = fn("touch_notes", {
  returns: "trigger",
  language: "plpgsql",
  security: "definer", // [!code focus]
  searchPath: "public", // [!code focus]
  body: sql`begin new.updated_at = now(); return new; end`,
  dependsOn: [{ table: notes, column: "updated_at" }], // [!code focus]
});

export default schema({
  tables: [notes],
  casing: "snake",
  functions: [slugify, touch],
  triggers: [
    trigger("notes_touch", {
      on: notes,
      timing: "before",
      events: ["update"],
      level: "row",
      updateOf: ["title"],
      when: sql`old.title is distinct from new.title`,
      calls: touch,
    }),
  ],
});
```

<Callout type="error" title="OKM1823">
Definer with no `searchPath` is `Function touch_notes is security definer and has no search_path.`
</Callout>


# Overview (/docs/define/schema)

<Anatomy call="schema" shape="object" exported>
  <AnatomyPart slot="tables" label="Tables" code="tables: [notes]," mark="required">
    The list, in the order you write it. A reference names a table in this list, earlier or later. [`table()`](/docs/define/table) only stores the columns. Names and the catalog hash are fixed here. Calling `schema()` twice on the same tables returns equal catalog bytes.
  </AnatomyPart>
  <AnatomyPart slot="sections" label="Pages" code="…," mark="sections">
    Each other field is a page in this group.
  </AnatomyPart>
</Anatomy>

## The declaration

Each option is a page. Tables come first, because every other option refers to them.

| Order | Page | You use it for |
| --- | --- | --- |
| 1 | [Tables](/docs/define/schema/tables) | The list `schema()` compiles. |
| 2 | [Casing](/docs/define/schema/casing) | The SQL name of a camelCase field. |
| 3 | [Codecs](/docs/define/schema/codecs) | The TypeScript value. The SQL type stays. |
| 4 | [Row types](/docs/define/schema/types) | Emitted types, or types kept in the checker. |
| 5 | [Id default](/docs/define/schema/defaults) | What a bare `t.id()` means. |
| 6 | [Minimum Postgres](/docs/define/schema/min-postgres) | The oldest major. Omitted means 15. |
| 7 | [Traits](/docs/define/schema/traits) | Columns added to every table. |
| 8 | [Tenancy](/docs/define/schema/tenancy) | Tenant keys on every table. |
| 9 | [Validation](/docs/define/schema/validation) | Whether rules run. |
| 10 | [Extensions](/docs/define/schema/extensions) | Extensions the catalog names. |
| 11 | [Functions](/docs/define/schema/functions) | Routines the catalog stores. |
| 12 | [Triggers](/docs/define/schema/triggers) | Triggers that call those functions. |
| 13 | [Views](/docs/define/schema/views) | Views and materialized views. |
| 14 | [Several catalogs](/docs/define/schema/catalogs) | `external()`, a snapshot, and one history per namespace. |


# Minimum Postgres (/docs/define/schema/min-postgres)

`minPostgres` is a major. [Connect](/docs/define/connect) checks the server against it. Omitted means 15, the oldest major okmodel runs. A feature below its major is refused when `schema()` runs. A bare `t.id()` stores `gen_random_uuid()` and does not need 18.

<Callout type="info" title="Not the catalog">
This field is not part of the catalog hash. Changing the floor does not change it.
</Callout>

### omitted

Omitted names 15. A bare `t.id()` stores `gen_random_uuid()`.

Connect accepts PostgreSQL 15 and newer. `okm migrate apply` still looks for an explicit `default uuidv7()` before the first statement. A server below 18 that has no `uuidv7()` function is refused, and nothing is changed. A server that already defines `uuidv7()` is accepted. A migration with no such expression does not ask the server.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.id(),
  title: t.text(),
});

export default schema({
  tables: [notes],
});
```

<Callout type="error" title="OKM1803">
A server below 15, when no floor is named, is `The server is PostgreSQL 14. This schema needs PostgreSQL 15 or higher (minPostgres: 15).`
</Callout>

<Callout type="error" title="OKM1812">
A server below 18 with no `uuidv7()` function is `Column notes.id has a uuidv7() default (migration 0001_init, step 0), and the server is PostgreSQL 17. uuidv7() needs PostgreSQL 18 or higher. minPostgres: 18. Nothing was changed.` The message names the column, the migration, the step, and the server.
</Callout>

### floor

`minPostgres: 18` asks for 18 or newer. A server below that major is refused. The majors are 15, 16, 17, and 18.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.id(),
  title: t.text(),
});

export default schema({
  tables: [notes],
  minPostgres: 18, // [!code focus]
});
```

### features

Each row is a feature that refuses a lower floor. The number is the major that feature needs.

| Feature | Needs |
| --- | --- |
| `uuidv7()` database default | Needs PostgreSQL 18 or higher. |

#### below 18

`minPostgres` of 15, 16, or 17 refuses a column whose default is `uuidv7()` when `schema()` runs. A bare `t.id()` is `gen_random_uuid()` and is accepted. `"uuidv4"` stores the same function. A client generator is the other way through. Both are on [id default](/docs/define/schema/defaults). An omitted floor does not refuse an explicit `uuidv7()` here. Apply checks the server.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.id({ default: "uuidv4" }), // [!code focus]
  title: t.text(),
});

export default schema({
  tables: [notes],
});
```

<Callout type="error" title="OKM1812">
`uuidv7() needs PostgreSQL 18 or higher. minPostgres: 18.`
</Callout>

<Callout type="error" title="OKM1802">
A server below the floor is `The server is PostgreSQL 16. This schema needs PostgreSQL 17 or higher (minPostgres: 17).`
</Callout>

### together

A schema written for 17. Both bare `t.id()` columns already store `gen_random_uuid()`. This example sets that same function on the schema. A large write-heavy table can set `uuidv7` instead, and that choice needs Postgres 18 and `minPostgres: 18`.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.id(),
  title: t.text(),
});

const sessions = table("sessions", {
  id: t.id(),
  name: t.text(),
});

export default schema({
  tables: [notes, sessions],
  minPostgres: 17, // [!code focus]
  defaults: { id: "uuidv4" },
});
```

### string

`requires: { postgres: ">=17" }` is the same floor as `minPostgres: 17`. The string form is deprecated. Passing both is an error that names both keys. `">= 17"`, `"17"`, `">=17.2"`, and a major outside 15 through 18 are errors. The error names `minPostgres` and those majors.

<Callout type="warning" title="OKM1060">
A major outside the list is `minPostgres: 15-18.` Passing both keys is `minPostgres or requires.postgres, not both.`
</Callout>


# Tables (/docs/define/schema/tables)

`tables` is every [`table()`](/docs/define/table) this schema compiles. The client name is the name you passed to `table()`.

The list order is the order of the emitted interfaces. The catalog is sorted by identity, so swapping two tables does not change the catalog hash. Calling `schema()` twice on the same tables returns equal catalog bytes.

| Field | Required | Default | Role |
| --- | --- | --- | --- |
| `tables` | yes | — | The list. A reference names one entry, earlier or later. |

### one

One table is a schema. The module default-exports it. [Config](/docs/define/config/schema) points `schema` at this file.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({ tables: [notes] }); // [!code focus]
```

### several

`.references("notes")` looks up `notes` in this list. `comments` is first here. `notes` is second. Either order resolves.

```ts title="several.ts"
import { schema, table, t } from "okmodel/pg";

const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes"), // [!code focus]
  body: t.text(),
});

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({ tables: [comments, notes] }); // [!code focus]
```

<Callout type="error" title="OKM1020">
A name that is not in the list is `Table posts is not in the schema.` The message lists the accepted names.
</Callout>

<Callout type="error" title="OKM1023">
The same name twice is `Table notes is declared more than once.` `note-items` and `note_items` both emit `NoteItems`, which is `Table note_items and note-items both emit NoteItems.` Two tables that store the same SQL name are `Table posts uses SQL name notes, which another table already uses.`
</Callout>

<Callout type="error" title="OKM1060">
A value that is not a list is `schema() needs a tables array.`
</Callout>

<Callout type="info" title="Empty">
An empty list is a schema with no tables.
</Callout>


# Tenancy (/docs/define/schema/tenancy)

`tenancy` is `columnTenancy()`, `compositeTenancy()`, or `rlsTenancy()` from `okmodel/tenancy`. Omitted, no table gains a key. Importing the module is what loads the predicate.

Each key is a guarded uuid. Insert fills it from `for()`. Reads and writes filter on it. Input cannot set it. A unique and the primary key include every key. An index must start with the key. `global("reason")` and `via()` are the table marks. Both are on [tenancy](/docs/define/table/tenancy).

### omitted

No tenant column, and `for()` is not on the client. A table that still sets `tenancy` is refused.

```ts title="omitted.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({
  tables: [notes],
});
```

<Callout type="error" title="OKM1060">
`Table notes sets tenancy, and the schema does not.` A value that is not `columnTenancy()`, `compositeTenancy()`, or `rlsTenancy()` is `schema() tenancy must be columnTenancy() from okmodel/tenancy.`
</Callout>

### column

`columnTenancy({ key, type: "uuid" })` adds one key. `type` is `"uuid"`.

```ts title="column.ts"
import { columnTenancy } from "okmodel/tenancy";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({
  tables: [notes],
  tenancy: columnTenancy({ key: "tenantId", type: "uuid" }), // [!code focus]
});
```

`for({ tenantId })` opens a client for one tenant. The value is a uuid. `unscoped("reason")` drops the predicate. Insert still needs `for()`. The root client keeps a global table and hides a tenant table.

```ts title="for.ts"
import { connect } from "okmodel/pg/postgresjs";
import { columnTenancy, global } from "okmodel/tenancy";
import { schema, table, t } from "okmodel/pg";

const plans = table(
  "plans",
  { id: t.identity(), code: t.text() },
  { tenancy: global("plans are shared") }, // [!code focus]
);

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({
  tables: [plans, notes],
  tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
});

export async function add(url: string, tenantId: string, title: string) {
  await using db = connect(url, { schema: app });
  await db.plans.find({ limit: 1 });
  return db.for({ tenantId }).notes.insert({ title }); // [!code focus]
}
```

<Callout type="error" title="OKM1060">
`columnTenancy() needs { key, type }.` Another type is `columnTenancy() type text must be uuid.` An empty key is `columnTenancy() key must be a field name.` A table mark that is neither `global("reason")` nor `via()` is `Table notes tenancy must be global("reason") or via().` An empty reason is `global() needs a reason.` A global unique with no reason is `Unique notes.email sets global and needs a reason.` `set null` on a tenant foreign key is `Foreign key notes.authorId includes tenantId, so set null cannot clear it. Accepted actions: cascade, restrict, no action.`
</Callout>

<Callout type="error" title="OKM1012">
`Table notes already declares tenantId, which tenancy adds. Rename the field or drop it from the table.`
</Callout>

<Callout type="error" title="OKM1190">
Insert that names the key is `Field notes.tenantId is the tenant key. The scope sets it. Input cannot.`
</Callout>

<Callout type="error" title="OKM1704">
An update is `Field notes.tenantId is the tenant key. An update cannot change it.` A filter is `Field notes.tenantId is the tenant key. A filter cannot set it.`
</Callout>

<Callout type="error" title="OKM1701">
No scope is `Table notes is tenant-scoped. Call for({ tenantId }) or unscoped("reason").` Insert without one is `insert on notes has no tenant. Call for({ tenantId }) so the scope can set it.` `for()` needs `{ tenantId }`, and the value must be a uuid: `for() needs { tenantId } as a uuid.` Another field is `for() accepts tenantId. organizationId is not the tenant key.` An empty reason is `unscoped() needs a reason.`
</Callout>

<Callout type="error" title="OKM1705">
A global table that references a tenant table is `Table plans.code is global and references tenant table notes. Drop the reference, or make plans a tenant table.`
</Callout>

<Callout type="error" title="OKM1706">
An index that does not lead with the key is `Index on notes starts with title. Put tenantId first.` With [casing](/docs/define/schema/casing) `"snake"`, the name in that sentence is `tenant_id`.
</Callout>

### composite

`compositeTenancy({ key, type: "uuid" })` adds every name, in that order. `for()` takes every key. One name is `columnTenancy()`.

```ts title="composite.ts"
import { connect } from "okmodel/pg/postgresjs";
import { compositeTenancy } from "okmodel/tenancy";
import { schema, table, t } from "okmodel/pg";

const documents = table("documents", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({
  tables: [documents],
  tenancy: compositeTenancy({
    key: ["organizationId", "workspaceId"], // [!code focus]
    type: "uuid",
  }),
});

export async function add(
  url: string,
  organizationId: string,
  workspaceId: string,
  title: string,
) {
  await using db = connect(url, { schema: app });
  return db.for({ organizationId, workspaceId }).documents.insert({ title }); // [!code focus]
}
```

<Callout type="error" title="OKM1060">
`compositeTenancy() needs { key, type }.` Another type is `compositeTenancy() type text must be uuid.` A key that is not a list is `compositeTenancy() key must be a list of field names, such as ["organizationId", "workspaceId"].` An empty name is `compositeTenancy() key must be a field name.` A repeated name is `compositeTenancy() key repeats organizationId.`
</Callout>

<Callout type="error" title="OKM1701">
`for()` without both keys is `for() needs { organizationId, workspaceId } as a uuid.` Another field is `for() accepts organizationId, workspaceId. extra is not the tenant key.`
</Callout>

### row level

`rlsTenancy({ key, type: "uuid" })` is one key. The column predicate stays. Policies are the backstop: `notes_tenant` and `notes_unscoped_select` on a table named `notes`. A view that exposes the key is `security_invoker`. `via()` and a key list are refused. `unscoped()` can read and cannot write.

```ts title="rls.ts"
import { rlsTenancy } from "okmodel/tenancy";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({
  tables: [notes],
  tenancy: rlsTenancy({ key: "tenantId", type: "uuid" }), // [!code focus]
});
```

A healthy role check is cached for that endpoint until its generation changes. A failed check is not cached. The message names the endpoint.

<Callout type="error" title="OKM1060">
`rlsTenancy() needs { key, type }.` A key list is `rlsTenancy() takes one key. Combining it with compositeTenancy or via() is not supported yet.` A path table is `Table tasks uses via() with rlsTenancy(). Combining them is not supported yet.`
</Callout>

<Callout type="error" title="OKM1707">
The owner is `The connected role owns the tenant tables, so row-level security would not apply.` A superuser is `The connected role is a superuser, so row-level security would not apply.` `BYPASSRLS` is `The connected role has BYPASSRLS, so row-level security would not apply.` Each sentence then names the endpoint: `Endpoint primary.` A role `pg_roles` does not list is `The connected role is not visible in pg_roles. Endpoint primary.`
</Callout>

<Callout type="error" title="OKM1708">
A policy that does not match is `Table notes does not force row-level security. Endpoint primary.` The words before the endpoint are `does not force row-level security.`, `is missing forced row-level security.`, `policy notes_tenant does not match the tenant predicate.`, `policy notes_unscoped_select does not match the unscoped predicate.`, or `has a row-level security policy okm did not emit.`
</Callout>

<Callout type="error" title="OKM1701">
`unscoped() cannot write under row-level security.`
</Callout>

### path

`via("project.organization")` follows to-one relations, at most three. The last table stores the key. This table does not. A read or a write adds `exists` along that path.

```ts title="path.ts"
import { columnTenancy, via } from "okmodel/tenancy";
import { one, schema, table, t } from "okmodel/pg";

const organizations = table("organizations", {
  id: t.identity(),
  name: t.text(),
});

const projects = table(
  "projects",
  {
    id: t.identity(),
    organizationId: t.bigint().references("organizations"),
    name: t.text(),
  },
  { relations: { organization: one("organizations") } },
);

const tasks = table(
  "tasks",
  {
    id: t.identity(),
    projectId: t.bigint().references("projects"),
    title: t.text(),
  },
  {
    relations: { project: one("projects") },
    tenancy: via("project.organization"), // [!code focus]
  },
);

export default schema({
  tables: [organizations, projects, tasks],
  tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
});
```

<Callout type="error" title="OKM1060">
An empty path is `via() needs a relation path.`
</Callout>

<Callout type="error" title="OKM1705">
More than three steps is `Table tasks tenancy via "a.b.c.d" is longer than 3 hops.` A missing to-one relation is `Table tasks tenancy via "project.organization" has no to-one relation project.` A path that ends on a global table is `Table tasks tenancy via "project.organization" ends at plans, which is not a tenant table.`
</Callout>


# Traits (/docs/define/schema/traits)

`traits` is a list. Schema traits apply first. A table's own list applies after them. An omitted list and `[]` add nothing. Import `okmodel/traits` to load the columns. A schema that never imports a trait does not carry that code.

`timestamps()`, `archivable()`, `versioned()`, and `sortable()` are the built-ins. A trait you write uses the same list. The builders are on [traits](/docs/define/table/traits). `archivable()` and `sortable()` rewrite uniques after tenancy has widened them.

### omitted

No list, or `[]`, leaves each table's own columns.

```ts title="omitted.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({
  tables: [notes],
});
```

### list

Each entry is a trait. `timestamps()` adds `createdAt` and `updatedAt`. Both are `timestamptz not null default now()`, guarded, and sealed. Insert leaves them to the default. Update sets `updatedAt` and leaves `createdAt`.

```ts title="list.ts"
import { schema, table, t } from "okmodel/pg";
import { timestamps } from "okmodel/traits";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({
  tables: [notes],
  traits: [timestamps()], // [!code focus]
});
```

#### trigger

`{ enforce: "trigger" }` also writes `updatedAt` in the database. The catalog gains `okm_touch_<column>` and one before-update trigger per table, named `<table>_touch`. `<column>` and `<table>` are the SQL names. With [casing](/docs/define/schema/casing) `"snake"`, the function is `okm_touch_updated_at`.

```ts title="trigger.ts"
import { schema, table, t } from "okmodel/pg";
import { timestamps } from "okmodel/traits";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({
  tables: [notes],
  traits: [timestamps({ enforce: "trigger" })], // [!code focus]
});
```

<Callout type="error" title="OKM1060">
Another key is `timestamps() option extra is not supported. Accepted options: enforce.` Another value is `timestamps() enforce database must be trigger.`
</Callout>

### opt out

`omitDefaults` is a reason. It drops the schema list and keeps traits declared on that table. An empty reason, or one that is only spaces, is refused.

```ts title="opt-out.ts"
import { schema, table, t } from "okmodel/pg";
import { timestamps } from "okmodel/traits";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const sessions = table(
  "sessions",
  { id: t.identity(), token: t.text() },
  { omitDefaults: "sessions are not stamped" }, // [!code focus]
);

export default schema({
  tables: [notes, sessions],
  traits: [timestamps()],
});
```

<Callout type="error" title="OKM1060">
A value that is not a list is `schema() traits must be a list of traits.` The same on a table is `Table notes traits must be a list of traits.` An empty reason is `Table sessions omitDefaults needs a reason.`
</Callout>

### together

Schema traits apply to `notes`. `tasks` adds `sortable` after them, so it also has `position`. `sessions` opts out, so it has neither `createdAt` nor `label`.

```ts title="together.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { sortable, timestamps, trait } from "okmodel/traits";

const labeled = trait("labeled", {
  fields: { label: t.text().default("note") },
});

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const tasks = table(
  "tasks",
  { id: t.identity(), title: t.text(), laneId: t.text() },
  { traits: [sortable("laneId")] }, // [!code focus]
);

const sessions = table(
  "sessions",
  { id: t.identity(), token: t.text() },
  { omitDefaults: "sessions are not stamped" },
);

const app = schema({
  tables: [notes, tasks, sessions],
  traits: [timestamps(), labeled], // [!code focus]
});

export async function open(url: string, title: string, token: string) {
  await using db = connect(url, { schema: app });
  const note = await db.notes.insert({ title });
  const session = await db.sessions.insert({ token });
  return { createdAt: note.createdAt, token: session.token };
}
```

<Callout type="error" title="OKM1012">
A column the table already has is `Trait labeled adds notes.label, and notes already declares it.` A column another trait already added is `Trait labeled adds notes.label, and trait timestamps already adds it.`
</Callout>

### Create your own

`timestamps()`, `archivable()`, `versioned()`, and `sortable()` are the built-ins above. A trait you write is `trait(name, { fields })` from `okmodel/traits`, the same function those built-ins call. Put it on this list and it is copied onto every table that does not opt out. The field options are on [traits](/docs/define/table/traits).

#### Simple

One text column with a default. Insert omits it, and the insert is awaited: `await using` closes the client when the function returns, so a returned promise would close it before the row is written. `okm generate` adds `"label" text not null default 'note'` on `notes`.

```ts title="own.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { trait } from "okmodel/traits";

const labeled = trait("labeled", {
  fields: { label: t.text().default("note") }, // [!code focus]
});

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export const app = schema({
  tables: [notes],
  traits: [labeled], // [!code focus]
});

export async function add(url: string, title: string) {
  await using db = connect(url, { schema: app });
  return await db.notes.insert({ title }); // [!code focus]
}
```

#### Advanced

`stamped` seals `label`, so input cannot set it, and touches `labeledAt`, so an update sets that column to `now()`. `timestamps()` is the same list, applied first, so the table also has `createdAt` and `updatedAt`.

```ts title="own-advanced.ts"
import { schema, table, t } from "okmodel/pg";
import { timestamps, trait } from "okmodel/traits";

const stamped = trait("stamped", {
  fields: {
    label: t.text().default("note"), // [!code focus]
    labeledAt: t.timestamptz().defaultSql("now()"), // [!code focus]
  },
  touch: ["labeledAt"], // [!code focus]
  sealed: ["label"], // [!code focus]
});

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export default schema({
  tables: [notes],
  traits: [timestamps(), stamped], // [!code focus]
});
```

<Callout type="error" title="OKM1012">
A column the table already declares is `Trait stamped adds notes.label, and notes already declares it.`
</Callout>


# Triggers (/docs/define/schema/triggers)

`triggers` is a list of `trigger()` from `okmodel/fn`. Identity is the table plus the name. The function it calls has to be in [functions](/docs/define/schema/functions). The catalog record is built when the catalog is first read.

`on`, `timing`, `events`, `level`, and `calls` are required. `timing` is `"before"`, `"after"`, or `"instead"`. `"instead"` is written `instead of`. `events` is `"insert"`, `"update"`, `"delete"`, or `"truncate"`, stored in that order. `level` is `"row"` or `"statement"`.

### call

One before-update row trigger. `calls` is a function in this schema.

```ts title="call.ts"
import { fn, trigger } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
  updatedAt: t.timestamptz().defaultSql("now()"),
});

const touch = fn("touch_notes", {
  returns: "trigger",
  language: "plpgsql",
  body: sql`begin new."updatedAt" = now(); return new; end`,
  dependsOn: [notes],
});

export default schema({
  tables: [notes],
  functions: [touch],
  triggers: [
    trigger("notes_touch", {
      on: notes, // [!code focus]
      timing: "before", // [!code focus]
      events: ["update"], // [!code focus]
      level: "row", // [!code focus]
      calls: touch, // [!code focus]
    }),
  ],
});
```

<Callout type="error" title="OKM1020">
A function that is not in the catalog is `Trigger notes_touch calls touch_notes, which is not in the catalog.` No event is `Trigger notes_touch needs an event.` Another timing is `Trigger notes_touch timing deferrable is not supported.` Another level is `Trigger notes_touch level each is not supported.`
</Callout>

<Callout type="error" title="OKM1023">
The same name on the same table is `Duplicate identity for trigger notes_touch.`
</Callout>

### update of

`updateOf` is SQL column names for `UPDATE OF`. Each one is a column dependency. The trigger needs an `update` event and `level: "row"`. With [casing](/docs/define/schema/casing) `"snake"`, `updatedAt` is `updated_at`.

```ts title="update-of.ts"
import { fn, trigger } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
  updatedAt: t.timestamptz().defaultSql("now()"),
});

const touch = fn("touch_notes", {
  returns: "trigger",
  language: "plpgsql",
  body: sql`begin new.updated_at = now(); return new; end`,
  dependsOn: [{ table: notes, column: "updated_at" }],
});

export default schema({
  tables: [notes],
  casing: "snake",
  functions: [touch],
  triggers: [
    trigger("notes_touch", {
      on: notes,
      timing: "before",
      events: ["update"],
      level: "row",
      updateOf: ["updated_at"], // [!code focus]
      calls: touch,
    }),
  ],
});
```

<Callout type="error" title="OKM1020">
A column the table does not have is `Trigger notes_touch names column missing, which notes does not have.` Columns without an update event are `Trigger notes_touch lists UPDATE OF columns without an update event.` Statement level is `Trigger notes_touch UPDATE OF columns need level row.`
</Callout>

### when

`when` is optional SQL, stored as written and rendered `when ( … )`.

```ts title="when.ts"
import { fn, trigger } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const touch = fn("touch_notes", {
  returns: "trigger",
  language: "plpgsql",
  body: sql`begin return new; end`,
  dependsOn: [notes],
});

export default schema({
  tables: [notes],
  functions: [touch],
  triggers: [
    trigger("notes_touch", {
      on: notes,
      timing: "before",
      events: ["update"],
      level: "row",
      when: sql`old.title is distinct from new.title`, // [!code focus]
      calls: touch,
    }),
  ],
});
```

### together

Several events, `updateOf`, and `when`. The column name is the SQL name. This trigger also fires on insert, so `when` names `new` only.

```ts title="together.ts"
import { fn, trigger } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
  updatedAt: t.timestamptz().defaultSql("now()"),
});

const touch = fn("touch_notes", {
  returns: "trigger",
  language: "plpgsql",
  body: sql`begin new.updated_at = now(); return new; end`,
  dependsOn: [{ table: notes, column: "updated_at" }],
});

export default schema({
  tables: [notes],
  casing: "snake", // [!code focus]
  functions: [touch],
  triggers: [
    trigger("notes_touch", {
      on: notes,
      timing: "before",
      events: ["insert", "update"], // [!code focus]
      level: "row",
      updateOf: ["updated_at"],
      when: sql`new.title <> ''`,
      calls: touch,
    }),
  ],
});
```

### Create your own

There is no built-in trigger on this list. `trigger()` from `okmodel/fn` is the declaration. The function it calls has to be in [functions](/docs/define/schema/functions). `timestamps({ enforce: "trigger" })` adds its own function and trigger; that option is on [traits](/docs/define/schema/traits).

#### Simple

One before-update row trigger. `okm generate` writes the table, then the function, then `create trigger "notes_touch" before update on "public"."notes" for each row execute function "public"."touch_notes"()`.

```ts title="own.ts"
import { fn, trigger } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const touch = fn("touch_notes", {
  returns: "trigger",
  language: "plpgsql",
  body: sql`begin return new; end`,
  dependsOn: [notes],
});

export default schema({
  tables: [notes],
  functions: [touch],
  triggers: [
    trigger("notes_touch", {
      on: notes, // [!code focus]
      timing: "before",
      events: ["update"],
      level: "row",
      calls: touch,
    }),
  ],
});
```

#### Advanced

`updateOf` limits the update to those SQL columns. Insert is still an event, so the trigger also runs then, and `updateOf` applies only to the update. `when` is stored as written. This trigger fires on insert, and an insert has no old row, so `when` names `new` only. `okm generate` writes `create trigger "notes_touch" before insert or update of "title" on "public"."notes" for each row when (new.title <> '') execute function "public"."touch_notes"()`.

```ts title="own-advanced.ts"
import { fn, trigger } from "okmodel/fn";
import { schema, sql, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const touch = fn("touch_notes", {
  returns: "trigger",
  language: "plpgsql",
  body: sql`begin return new; end`,
  dependsOn: [notes],
});

export default schema({
  tables: [notes],
  functions: [touch],
  triggers: [
    trigger("notes_touch", {
      on: notes,
      timing: "before",
      events: ["insert", "update"], // [!code focus]
      level: "row",
      updateOf: ["title"], // [!code focus]
      when: sql`new.title <> ''`, // [!code focus]
      calls: touch,
    }),
  ],
});
```

<Callout type="error" title="OKM1020">
A column the table does not have is `Trigger notes_touch names column missing, which notes does not have.`
</Callout>


# Row types (/docs/define/schema/types)

`types` is stored on the schema. Queries still infer a row from `schema()`. `okm build` writes `.okm/types.d.ts` for either value, and `okm generate` writes that same file. The header is `Mode: emitted` or `Mode: inferred`. The interfaces do not change with the mode.

A table named `notes` becomes `Notes`, `NotesInsert`, and `NotesUpdate`. The file then maps those names on `Rows`, `Inserts`, and `Updates`, in the order of the [tables](/docs/define/schema/tables) list. Two names that emit one interface are refused there.

`t.jsonb<{ published?: boolean }>()` keeps that argument in the file. The type parameter is erased at runtime, so the text is read from the schema source. A bare `t.jsonb()` is `jsonb<unknown>`. When either form appears, the file starts with `export type jsonb<T> = T`. `t.json<T>()` is the same, with `json`.

<Callout type="info" title="Not the catalog">
This flag is not part of the catalog hash. Swapping `"emitted"` and `"inferred"` does not change it.
</Callout>

### emitted

Omitted means `"emitted"`. This is the mode a file imports when it should not re-check the schema.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export const app = schema({
  tables: [notes],
});

export const mode = app.types;
```

`"emitted"` stores the same value.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export const app = schema({
  tables: [notes],
  types: "emitted", // [!code focus]
});

export const mode = app.types;
```

### inferred

`"inferred"` stores that mode. The client and `Row`, `Insert`, and `Update` still read the schema. Use this when the checker should see the schema, not the written interfaces.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export const app = schema({
  tables: [notes],
  types: "inferred", // [!code focus]
});

export const mode = app.types;
```

### together

Two tables, a typed `jsonb`, and the default mode. `okm build` writes `Notes` and `Comments`, each with an insert and an update, then `Rows`, `Inserts`, and `Updates`.

```ts title="schema.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
  meta: t.jsonb<{ published?: boolean }>(), // [!code focus]
});

const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes"),
  body: t.text(),
});

export default schema({
  tables: [notes, comments],
});
```

<Callout type="warning" title="OKM1060">
Any other value is `schema() types local must be emitted or inferred.` The type only allows `"emitted"` and `"inferred"`, so this is also a type error.
</Callout>


# Validation (/docs/define/schema/validation)

`validation` is stored as you pass it. `schema()` does not read the rules. `okm check` does, and the engine does on the first validated call. Import `okmodel/validate` before that call, or a write that would validate is refused. The rules themselves are [on the table](/docs/define/table/validation).

Omitted and `false` are off. `true` is on. An object is on unless `enabled` is `false`. Omitting `enabled` inside an object leaves it on. Insert and update run the rules. A read does not. `{ validate: false }` skips that call. `onRead` is stored. The write path does not read it.

`style` chooses where a rule may live. `"inline"` is `.validate()` on the column. `"section"` is the table `validate` section. Omitted, either place is accepted, and the same field in both is refused.

### off

Omitted, rules stay off. `false` is the same.

```ts title="off.ts"
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([v.min(1, "title_required")]),
});

export default schema({
  tables: [notes],
});
```

`false` leaves them off as well.

```ts title="disabled.ts"
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([v.min(1, "title_required")]),
});

export default schema({
  tables: [notes],
  validation: false, // [!code focus]
});
```

### on

`true` turns rules on. `onRead` stays `false`. `style` stays unset, so a column rule and a `validate` section may both exist, on different fields.

```ts title="on.ts"
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([v.min(1, "title_required")]), // [!code focus]
});

export default schema({
  tables: [notes],
  validation: true, // [!code focus]
});
```

### object

`enabled`, `onRead`, and `style` are the keys. `onRead: true` is stored with the flag. A read still does not run the rules.

#### inline

`"inline"` keeps column rules and refuses a `validate` section, including `$row`.

```ts title="inline.ts"
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([v.trim(), v.min(1, "title_required")]),
});

export default schema({
  tables: [notes],
  validation: { enabled: true, onRead: false, style: "inline" }, // [!code focus]
});
```

#### section

`"section"` keeps the table `validate` section and refuses `.validate()` on a column.

```ts title="section.ts"
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  { validate: { title: [v.min(1, "title_required")] } },
);

export default schema({
  tables: [notes],
  validation: { style: "section" }, // [!code focus]
});
```

<Callout type="error" title="OKM1060">
Anything other than a boolean or an object is `schema() validation must be a boolean or { enabled, onRead, style }.` Another style is `validation.style must be inline or section.` The same on a table is `table() validation must be a boolean or { enabled, onRead, style }.`
</Callout>

<Callout type="error" title="OKM1030">
The same field in both places is `Field notes.title is validated in two places. Keep the column rules or the validate section.` `"inline"` with a `validate` section, including `$row`, is the same code. `"section"` with `.validate()` is the same code.
</Callout>

### table

A table flag replaces `enabled` and keeps the schema's `onRead` and `style`. `true` turns that table on. `false` turns it off. An object can set its own `onRead` and `style`. Omitting either one keeps the schema value.

```ts title="table.ts"
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([v.min(1, "title_required")]),
});

const drafts = table(
  "drafts",
  {
    id: t.identity(),
    title: t.text(),
  },
  { validation: false }, // [!code focus]
);

export default schema({
  tables: [notes, drafts],
  validation: { enabled: true, style: "inline" },
});
```

### together

`insert.validate` checks the row and returns it frozen. A later `insert` of that value skips the rules. `insert.check` returns the issues and does not throw.

```ts title="together.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.varchar(200).validate([v.trim(), v.min(1, "title_required")]),
});

const app = schema({
  tables: [notes],
  validation: true, // [!code focus]
});

export async function add(url: string, title: string) {
  await using db = connect(url, { schema: app });
  const row = await db.notes.insert.validate({ title }); // [!code focus]
  return db.notes.insert(row);
}
```

<Callout type="error" title="OKM1201">
A write that would validate, when `okmodel/validate` was not imported, is ``Validation is enabled but `okmodel/validate` was not imported.``
</Callout>

<Callout type="error" title="OKM1200">
A failed `validate` is `Validation failed: title title_required.` The message names the first field and its key. `check` returns the issues instead.
</Callout>


# Views (/docs/define/schema/views)

`views` is a list of `view()` and `materializedView()` from `okmodel/view`. You declare the output columns and the query. The query is stored as written. `schema()` does not check it against the tables, except for tenancy. An omitted list, an empty list, and a value that is not a list add no view.

`db.views` is the read handle. The client name is the camelCase of the view name, so `open_notes` is `openNotes`. Every output column is decoded as text. Each field is `string | null`. A plain view has `find`, `one`, `count`, and `exists`. A materialized view also has `refresh()`.

`columns`, `query` are required on the SQL form. The builder form is `view(name, (q) => q.from(table))`. A column list projects those fields. Omit it to select every field. `.where(sql`...`)` appends that text. `.tenancy(global("reason"))` opts out. The builder form is `public` only. It has no schema option. `materializedView()` stays the SQL form. On the SQL form, `schema` defaults to `public`. `tenancy` opts a view out with `global("reason")`.

### view

`view()` stores the query. `find` needs a `limit`, or `.all(reason)`.

```ts title="view.ts"
import { schema, sql, table, t } from "okmodel/pg";
import { view } from "okmodel/view";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const openNotes = view("openNotes", {
  columns: [
    { name: "id", type: "bigint" }, // [!code focus]
    { name: "title", type: "text" }, // [!code focus]
  ],
  query: sql`select id, title from notes`, // [!code focus]
});

export default schema({
  tables: [notes],
  views: [openNotes], // [!code focus]
});
```

### materialized

`materializedView()` is created `with no data`. Populate is a later plan step. Omitted `refresh` still populates, without `concurrently`. `db.views.noteCounts.refresh()` runs `refresh materialized view "noteCounts"`. The call sets `app.unscoped` for that statement and is not tenant-scoped.

```ts title="materialized.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, sql, table, t } from "okmodel/pg";
import { materializedView } from "okmodel/view";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const noteCounts = materializedView("noteCounts", {
  columns: [{ name: "n", type: "bigint" }],
  query: sql`select count(*)::bigint as n from notes`,
});

const app = schema({
  tables: [notes],
  views: [noteCounts],
});

export async function reload(url: string) {
  await using db = connect(url, { schema: app });
  await db.views.noteCounts.refresh(); // [!code focus]
  return db.views.noteCounts.find({ limit: 1 });
}
```

<Callout type="error" title="OKM1111">
A driver that cannot reserve a connection is `Refresh needs a driver that can reserve a connection.`
</Callout>

### concurrently

`refresh: "concurrently"` plans `refresh materialized view concurrently` once the view has been populated. The first populate is not concurrent. Postgres rejects `concurrently` until a unique index exists, so the declaration needs one. Any other refresh value is refused.

```ts title="concurrently.ts"
import { schema, sql, table, t } from "okmodel/pg";
import { materializedView } from "okmodel/view";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const noteCounts = materializedView("noteCounts", {
  columns: [{ name: "title", type: "text" }],
  query: sql`select title from notes`,
  indexes: [{ columns: ["title"], unique: true }], // [!code focus]
  refresh: "concurrently", // [!code focus]
});

export default schema({
  tables: [notes],
  views: [noteCounts],
});
```

<Callout type="error" title="OKM1822">
`Materialized view noteCounts refreshes concurrently and has no unique index.`
</Callout>

<Callout type="error" title="OKM1120">
Another refresh value is `Materialized view noteCounts refresh must be "concurrently".`
</Callout>

<Callout type="error" title="OKM1020">
An index column the view does not declare is `Index on noteCounts names column missing, which the materialized view does not have.`
</Callout>

### tenancy

A view that reads a tenant table has to expose the tenant key, or pass `global("reason")`. The reason is stored for `inspect()` and is not part of the catalog hash. Under `rlsTenancy`, a view that exposes the key is created `with (security_invoker = true)`.

```ts title="tenancy.ts"
import { columnTenancy, global } from "okmodel/tenancy";
import { schema, sql, table, t } from "okmodel/pg";
import { view } from "okmodel/view";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const openNotes = view("openNotes", {
  columns: [
    { name: "id", type: "bigint" },
    { name: "title", type: "text" },
    { name: "tenantId", type: "uuid" }, // [!code focus]
  ],
  query: sql`select id, title, "tenantId" from notes`,
});

const shared = view("shared", {
  columns: [{ name: "n", type: "bigint" }],
  query: sql`select count(*)::bigint as n from notes`,
  tenancy: global("counts are shared"), // [!code focus]
});

export default schema({
  tables: [notes],
  tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
  views: [openNotes, shared],
});
```

<Callout type="error" title="OKM1820">
A tenant view that hides the key is `View openNotes reads a tenant table and does not expose tenantId.`
</Callout>

<Callout type="error" title="OKM1020">
Anything other than `global("reason")` is `View openNotes tenancy must be global("reason").` Two output columns that camelCase to one field are `View column tenant_id repeats tenantId.`
</Callout>

<Callout type="error" title="OKM1023">
The same client name as a table is `View notes uses the client name notes.` The same SQL name as a table is `View notes uses the name of a table.`
</Callout>

### schema

`schema` is the install schema. The default is `public`.

```ts title="install.ts"
import { schema, sql, table, t } from "okmodel/pg";
import { view } from "okmodel/view";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const openNotes = view("openNotes", {
  schema: "app", // [!code focus]
  columns: [
    { name: "id", type: "bigint" },
    { name: "title", type: "text" },
  ],
  query: sql`select id, title from notes`,
});

export default schema({
  tables: [notes],
  views: [openNotes],
});
```

### together

A tenant view exposes `tenantId`. A materialized view opts out, refreshes concurrently, and is read after `refresh()`.

```ts title="together.ts"
import { connect } from "okmodel/pg/postgresjs";
import { columnTenancy, global } from "okmodel/tenancy";
import { schema, sql, table, t } from "okmodel/pg";
import { materializedView, view } from "okmodel/view";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const openNotes = view("openNotes", {
  columns: [
    { name: "id", type: "bigint" },
    { name: "title", type: "text" },
    { name: "tenantId", type: "uuid" },
  ],
  query: sql`select id, title, "tenantId" from notes`,
});

const noteCounts = materializedView("noteCounts", {
  columns: [{ name: "title", type: "text" }],
  query: sql`select title from notes`,
  indexes: [{ columns: ["title"], unique: true }],
  refresh: "concurrently",
  tenancy: global("counts are shared"),
});

const app = schema({
  tables: [notes],
  tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
  views: [openNotes, noteCounts],
});

export async function reload(url: string, tenantId: string) {
  await using db = connect(url, { schema: app });
  await db.views.noteCounts.refresh();
  return db.for({ tenantId }).views.openNotes.find({ limit: 1 }); // [!code focus]
}
```


# Checks (/docs/define/table/checks)

<Callout type="info" title="Where this fits">
An [index](/docs/define/table/indexes) does not reject a row. A check does. A [picklist](/docs/define/table/shape) already adds its own check.
</Callout>

`checks` is an object. Each key is a name you choose. Each value receives column handles and returns `sql` text. `schema()` stores that text. This is not a query builder. A return that is not `sql` text is OKM1060: `Check notes.title_present must return sql text.`

<Callout type="info" title="SQL name">
`sql` inserts a handle as the SQL name `schema()` stores, unquoted. A [casing](/docs/define/schema/casing) of `"snake"` has already been applied. A string is inserted as you wrote it. A number or a boolean is inserted as text.
</Callout>

### Name

The key is not the constraint name. The constraint is `{table}_{key}_check`. Snake casing does not rewrite the key.

#### key

The key below is `title_present`, so the constraint is `notes_title_present_check`.

```ts title="name.ts"
import { sql, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  {
    checks: {
      title_present: (columns) => sql`${columns.title} <> ''`, // [!code focus]
    },
  },
);
```

#### rename

Keep that key when you rename a column the check uses. The constraint stays `tasks_present_check`. The plan renames the column and does not drop the constraint.

```ts title="rename.ts"
import { sql, table, t } from "okmodel/pg";

export const tasks = table(
  "tasks",
  {
    id: t.identity(),
    contact: t.text().renamedFrom("email"), // [!code focus]
  },
  {
    checks: {
      present: (columns) => sql`${columns.contact} <> ''`, // [!code focus]
    },
  },
);
```

A new table adds the check with `alter table`. A table that already exists adds it `not valid`, then validates it.

A row that fails the check is `Check violation on notes (title_present)`. The kind is `check`. `constraint` is `notes_title_present_check`.

<Callout type="error" title="OKM1023">
A picklist check is `{table}_{sqlColumn}_check`. A key equal to that SQL column name is `Duplicate identity for constraint notes_title_check.`
</Callout>

<Callout type="error" title="OKM1122">
An empty key is `Name key is empty.`
</Callout>

### Expression

The function receives column handles and returns `sql` text.

#### one

One column compares the stored value with SQL you write.

```ts title="one-column.ts"
import { sql, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  {
    checks: {
      title_present: (columns) => sql`${columns.title} <> ''`, // [!code focus]
    },
  },
);
```

#### two

Two handles compare two columns. This check is `notes_ends_after_start_check`, and the text is `endsAt > startsAt` when those are the SQL names.

```ts title="two-columns.ts"
import { sql, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    startsAt: t.timestamptz(),
    endsAt: t.timestamptz(),
  },
  {
    checks: {
      ends_after_start: (columns) => sql`${columns.endsAt} > ${columns.startsAt}`, // [!code focus]
    },
  },
);
```

#### number

A number is inserted as text. This expression is `rank >= 1`.

```ts title="number.ts"
import { sql, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    rank: t.integer(),
  },
  {
    checks: {
      rank_positive: (columns) => sql`${columns.rank} >= ${1}`, // [!code focus]
    },
  },
);
```

#### text

Quotes around text belong in the template. `` sql`${columns.status} <> 'draft'` `` is `status <> 'draft'`. Interpolating the string `"draft"` inserts `draft` with no quotes.

```ts title="text.ts"
import { sql, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    status: t.text(),
  },
  {
    checks: {
      not_draft: (columns) => sql`${columns.status} <> 'draft'`, // [!code focus]
    },
  },
);
```

#### several

Several checks are several keys. Each key is its own constraint: `notes_title_present_check` and `notes_rank_positive_check`.

```ts title="several.ts"
import { sql, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
    rank: t.integer(),
  },
  {
    checks: {
      title_present: (columns) => sql`${columns.title} <> ''`, // [!code focus]
      rank_positive: (columns) => sql`${columns.rank} >= ${1}`, // [!code focus]
    },
  },
);
```

#### sql name

A [SQL name](/docs/define/table/names#sql-name) you set is the name inside the expression. Here the column is `published_at` and the table is `note`, so the constraint is `note_after_open_check` and the text uses `published_at`.

```ts title="sql-name.ts"
import { sql, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    openedAt: t.timestamptz(),
    publishedAt: t.timestamptz().sqlName("published_at"), // [!code focus]
  },
  {
    sqlName: "note", // [!code focus]
    checks: {
      after_open: (columns) => sql`${columns.publishedAt} > ${columns.openedAt}`,
    },
  },
);
```

#### casing

`"snake"` rewrites a name you did not set. `publishedAt` is `published_at` in the expression. The key stays `after_epoch`, so the constraint is `notes_after_epoch_check`.

```ts title="casing.ts"
import { schema, sql, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  {
    id: t.identity(),
    publishedAt: t.timestamptz(), // [!code focus]
  },
  {
    checks: {
      after_epoch: (columns) => sql`${columns.publishedAt} > '2020-01-01'`,
    },
  },
);

export const app = schema({
  tables: [notes],
  casing: "snake", // [!code focus]
});
```


# Columns (/docs/define/table/columns)

<Callout type="info" title="Where this fits">
The [key](/docs/define/table/keys) is one column. Each other field is a builder on this page. Use the table first, then the section for that storage. [Defaults](/docs/define/table/defaults) come after the type.
</Callout>

Each field is a data type. The builder name is that type. Postgres is the only database today. A cell is `yes` when that database has the type built in, and `extension` when the schema must list that extension. Another database is another column on the same row. A missing extension declaration is OKM1020. See [extensions](/docs/define/schema/extensions).

An array is [`.array()`](/docs/define/table/shape#array) on a scalar. A fixed set of strings on a text column is a [picklist](/docs/define/table/shape#picklist). A named Postgres enum is its own type, under [enum](/docs/define/table/shape#enum).

| You store | Builder |
| --- | --- |
| a string with no length cap | `t.text()` |
| a string with a maximum length | `t.varchar(n)` |
| a string compared without case | `t.citext()` |
| a tight integer | `t.smallint()` |
| a typical count | `t.integer()` |
| a 64-bit integer | `t.bigint()` |
| a generated integer key | `t.identity()` |
| money, a rate, or any exact quantity | `t.numeric(precision, scale)` |
| a measurement that may round | `t.double()` |
| an instant | `t.timestamptz()` |
| a calendar day | `t.date()` |
| a span | `t.interval()` |
| true or false | `t.boolean()` |
| bytes | `t.bytea()` |
| a document you query | `t.jsonb()` |
| a uuid that is not the primary key | `t.uuid()` |
| a primary key the application mints | `t.id({ default: okid() })` |
| a closed set of labels | `t.enum(name, values)` |

### Text

Postgres stores `text`, `varchar`, and `char` with the same representation. The length is a check, counted in characters. A value up to 126 bytes is 1 byte plus the string. A longer value is 4 bytes plus the string. `char` includes its padding in that string. `citext` uses the same size.

#### Unbounded

`t.text()` is `text`. The TypeScript value is a string. A title, a body, or a name uses it.

```ts title="unbounded.ts"
import { table, t } from "okmodel/pg";

export const people = table("people", {
  id: t.identity(),
  name: t.text(), // [!code focus]
});
```

#### Varchar

`t.varchar(n)` is `varchar(n)`. The TypeScript value is a string. `n` is an integer from 1 to 10485760.

```ts title="varchar.ts"
import { table, t } from "okmodel/pg";

export const people = table("people", {
  id: t.identity(),
  code: t.varchar(16), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
A length outside 1 to 10485760 is refused.
</Callout>

<Callout type="error" title="OKM1210">
A longer value is refused when it is encoded.
</Callout>

#### Char

`t.char(n)` is `char(n)`. The TypeScript value is a string. `n` is an integer from 1 to 10485760. Encode stores the characters you pass.

```ts title="char.ts"
import { table, t } from "okmodel/pg";

export const people = table("people", {
  id: t.identity(),
  region: t.char(2), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
A length outside 1 to 10485760 is refused.
</Callout>

<Callout type="error" title="OKM1210">
A longer value is refused when it is encoded.
</Callout>

#### Citext

`t.citext()` is `citext`. The TypeScript value is a string. Comparison ignores case. The schema lists `citext()` from `okmodel/pg/citext`. See [extensions](/docs/define/schema/extensions).

```ts title="citext.ts"
import { citext } from "okmodel/pg/citext";
import { schema, table, t } from "okmodel/pg";

const people = table("people", {
  id: t.identity(),
  email: t.citext(), // [!code focus]
});

export default schema({
  tables: [people],
  extensions: [citext()], // [!code focus]
});
```

<Callout type="error" title="OKM1020">
`t.citext()` is refused when `extensions` is omitted or empty.
</Callout>

<Callout type="error" title="OKM1810">
`t.citext()` is refused when `extensions` is a list that does not contain `citext`.
</Callout>

### Integers

The value is a whole number inside that range.

#### Smallint

`t.smallint()` is `smallint`. The range is -32768 to 32767. The TypeScript value is a number. Storage is 2 bytes.

```ts title="smallint.ts"
import { table, t } from "okmodel/pg";

export const counts = table("counts", {
  id: t.identity(),
  flag: t.smallint(), // [!code focus]
});
```

<Callout type="error" title="OKM1210">
A value that is not an integer in that range is refused when it is encoded.
</Callout>

#### Integer

`t.integer()` is `integer`. The range is -2147483648 to 2147483647. The TypeScript value is a number. Storage is 4 bytes. This is the usual count.

```ts title="integer.ts"
import { table, t } from "okmodel/pg";

export const counts = table("counts", {
  id: t.identity(),
  total: t.integer(), // [!code focus]
});
```

<Callout type="error" title="OKM1210">
A value that is not an integer in that range is refused when it is encoded.
</Callout>

#### Bigint

`t.bigint()` is `bigint`. The range is -9223372036854775808 to 9223372036854775807. The TypeScript value is a decimal string, so a value past a safe integer stays exact. Storage is 8 bytes. A bare call stays a string. A schema [bigint codec](/docs/define/schema/codecs) other than `"string"` needs the same `as` on this column. A [safe integer](#safe-integer) or a [JavaScript bigint](#javascript-bigint) selects the other modes.

```ts title="bigint.ts"
import { table, t } from "okmodel/pg";

export const counts = table("counts", {
  id: t.identity(),
  sequence: t.bigint(), // [!code focus]
});
```

<Callout type="error" title="OKM1210">
A string that is not a canonical integer is refused when it is encoded. The spelling has no leading zeros, for example `0` or `-12`.
</Callout>

#### Safe integer

`{ as: "number" }` on `t.bigint()` or `t.identity()` makes the TypeScript value a number. It accepts a safe integer only.

```ts title="safe-integer.ts"
import { table, t } from "okmodel/pg";

export const counts = table("counts", {
  id: t.identity(),
  sequence: t.bigint({ as: "number" }), // [!code focus]
});
```

<Callout type="error" title="OKM1210">
A value outside the safe integer range is refused when it is encoded.
</Callout>

#### JavaScript bigint

`{ as: "bigint" }` on `t.bigint()` or `t.identity()` makes the TypeScript value a JavaScript bigint.

```ts title="javascript-bigint.ts"
import { table, t } from "okmodel/pg";

export const counts = table("counts", {
  id: t.identity(),
  sequence: t.bigint({ as: "bigint" }), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
Any other `as` on `t.bigint()` or `t.identity()` is refused.
</Callout>

#### Identity

`t.identity()` is `bigint` `GENERATED ALWAYS AS IDENTITY` and a primary key. Insert and update omit it. The range and the `as` modes match [bigint](#bigint). A generated integer key uses this builder.

```ts title="identity.ts"
import { table, t } from "okmodel/pg";

export const counts = table("counts", {
  id: t.identity(), // [!code focus]
});
```

### Numbers

`t.numeric()` is exact. Money, rates, and quantities use it. Postgres `decimal` is this same type. The size is two bytes for each group of four decimal digits, plus 3 to 8 bytes. Leading and trailing zeros are not stored. A bare call stays a string. A schema [numeric codec](/docs/define/schema/codecs) of `"number"` needs `{ as: "number" }` on this column.

#### Unconstrained

`t.numeric()` is `numeric`. The TypeScript value is a string. Omit precision and scale for an unconstrained numeric. The string keeps every digit.

```ts title="unconstrained.ts"
import { table, t } from "okmodel/pg";

export const payments = table("payments", {
  id: t.identity(),
  amount: t.numeric(), // [!code focus]
});
```

#### Precision

`t.numeric(precision)` is `numeric(precision)`. Precision is an integer from 1 to 1000.

```ts title="precision.ts"
import { table, t } from "okmodel/pg";

export const payments = table("payments", {
  id: t.identity(),
  amount: t.numeric(12), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
A precision outside 1 to 1000 is refused.
</Callout>

#### Scale

`t.numeric(precision, scale)` is `numeric(precision, scale)`. Scale is an integer from 0 to that precision.

```ts title="scale.ts"
import { table, t } from "okmodel/pg";

export const payments = table("payments", {
  id: t.identity(),
  amount: t.numeric(12, 2), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
A scale outside 0 to the precision is refused.
</Callout>

#### Number

`{ as: "number" }` makes the TypeScript value a `number`. The SQL type stays the same. It can lose digits past a JavaScript number. Any other `as` is refused.

```ts title="number.ts"
import { table, t } from "okmodel/pg";

export const payments = table("payments", {
  id: t.identity(),
  amount: t.numeric(12, 2, { as: "number" }), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
`as` must be `"string"` or `"number"`.
</Callout>

### Floats

`t.real()` and `t.double()` are binary floating point. A sum of them is not exact. An exact quantity uses [numeric](#numbers). Postgres `float` is this family.

#### Real

`t.real()` is `real`, single precision, about 6 decimal digits. The TypeScript value is a number. Storage is 4 bytes.

```ts title="real.ts"
import { table, t } from "okmodel/pg";

export const samples = table("samples", {
  id: t.identity(),
  reading: t.real(), // [!code focus]
});
```

<Callout type="error" title="OKM1210">
Infinity and NaN are refused when the value is encoded.
</Callout>

#### Double

`t.double()` is `double precision`, about 15 decimal digits. The TypeScript value is a number. Storage is 8 bytes.

```ts title="double.ts"
import { table, t } from "okmodel/pg";

export const samples = table("samples", {
  id: t.identity(),
  reading: t.double(), // [!code focus]
});
```

<Callout type="error" title="OKM1210">
Infinity and NaN are refused when the value is encoded.
</Callout>

### Time

The value is a `Temporal` object. The schema [timestamps codec](/docs/define/schema/codecs) is `"temporal"`. [Fractional seconds](#fractional-seconds) apply to `timestamptz`, `timestamp`, `time`, and `timetz`.

Bun and Node 26 provide a `Temporal` global. Node 22 and 24 need a polyfill assigned to `globalThis.Temporal` before these columns run. okmodel does not ship one. A missing global is OKM1210.

```ts title="polyfill.ts"
import { Temporal } from "temporal-polyfill";

globalThis.Temporal = Temporal;
```

#### Instant

`t.timestamptz()` is `timestamptz`. The TypeScript value is `Temporal.Instant`. Postgres stores it in UTC. Storage is 8 bytes. A created time or an event uses it.

```ts title="instant.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  publishedAt: t.timestamptz(), // [!code focus]
});
```

#### Timestamp

`t.timestamp()` is `timestamp`. The TypeScript value is `Temporal.PlainDateTime`. It stores a civil date and time. Storage is 8 bytes.

```ts title="timestamp.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  editedAt: t.timestamp(), // [!code focus]
});
```

#### Date

`t.date()` is `date`. The TypeScript value is `Temporal.PlainDate`. It stores a calendar day. Storage is 4 bytes.

```ts title="date.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  day: t.date(), // [!code focus]
});
```

#### Time of day

`t.time()` is `time`. The TypeScript value is `Temporal.PlainTime`. It stores a clock time. The zone is its own column. Storage is 8 bytes.

```ts title="time-of-day.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  opens: t.time(), // [!code focus]
});
```

#### Offset

`t.timetz()` is `timetz`. The TypeScript value is `{ time, offset }`. `time` is a `Temporal.PlainTime`. `offset` is `+HH:MM` or `-HH:MM`. A daylight-saving change needs the zone in its own column. Storage is 12 bytes.

```ts title="offset.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  shift: t.timetz(), // [!code focus]
});
```

<Callout type="error" title="OKM1210">
An offset other than `±HH:MM` is refused when the value is encoded.
</Callout>

#### Interval

`t.interval()` is `interval`. The TypeScript value is `Temporal.Duration`. It stores a span. Storage is 16 bytes. `fields` is one of `year`, `month`, `day`, `hour`, `minute`, `second`, `year to month`, `day to hour`, `day to minute`, `day to second`, `hour to minute`, `hour to second`, `minute to second`.

```ts title="interval.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  length: t.interval("day to second"), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
Any other `fields` value is refused.
</Callout>

#### Fractional seconds

`precision` is fractional seconds, an integer from 0 to 6. Omit it for full precision. It applies to `t.timestamptz()`, `t.timestamp()`, `t.time()`, and `t.timetz()`.

```ts title="fractional-seconds.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  editedAt: t.timestamp(0), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
A precision outside 0 to 6 is refused.
</Callout>

### Boolean and binary

| Builder | SQL | TypeScript | Bytes | Postgres |
| --- | --- | --- | --- | --- |
| `t.boolean()` | `boolean` | `boolean` | 1 | yes |
| `t.bytea()` | `bytea` | `Uint8Array` | 1 or 4 + bytes | yes |

`t.boolean()` is true or false. The wire text may be `true`, `false`, `t`, `f`, `1`, or `0`. `t.bytea()` is raw bytes. Up to 126 bytes, the header is 1 byte. A longer value uses a 4-byte header. The wire text is `\x` plus lowercase hex.

```ts title="files.ts"
import { table, t } from "okmodel/pg";

export const files = table("files", {
  id: t.identity(),
  published: t.boolean().default(false), // [!code focus]
  body: t.bytea(), // [!code focus]
});
```

### JSON

| Builder | SQL | TypeScript | Bytes | Postgres |
| --- | --- | --- | --- | --- |
| `t.json()` | `json` | the `T` you name, or `unknown` | 1 or 4 + text | yes |
| `t.jsonb()` | `jsonb` | the `T` you name, or `unknown` | variable | yes |

`T` is the shape you pass. The sample names it. The codec does not check it. `t.jsonb()` is the document you query: Postgres stores it in a binary form and can index it. `t.json()` stores the text, including key order and insignificant space. A bigint inside the value is written as a decimal string. `undefined` and a function are refused.

```ts title="notes.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  meta: t.json<{ source: string }>(), // [!code focus]
  doc: t.jsonb<{ tags: string[] }>(), // [!code focus]
});
```

### UUID

| Builder | SQL | TypeScript | Bytes | Postgres |
| --- | --- | --- | --- | --- |
| `t.uuid()` | `uuid` | `string` | 16 | yes |
| `t.id()` | `uuid` | `string` | 16 | yes |

`t.uuid()` has no default. The text is 8-4-4-4-12 hexadecimal digits, 128 bits. `t.id()` is a primary key. A bare `t.id()` is `uuid`, 16 bytes. The default is covered in [keys](/docs/define/table/keys). An [OKID](#okid) default is `text`, not this row.

```ts title="notes.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.id(), // [!code focus]
  authorId: t.uuid(), // [!code focus]
});
```

### OKID

`okid` comes from `okmodel/ids`. It is not a Postgres type. `t.id({ default: okid() })` is a primary key of `text` with collation `C`. The client mints one id on insert. The catalog stores no default. Calling `okid()` returns the generator. It does not mint the id at definition time.

A default body is 21 characters on a URL-safe alphabet, so the stored value is 22 bytes. A longer body, or a prefix, follows [text](#text): 1 byte plus the string up to 126 bytes, then 4 bytes plus the string.

| Call | Body | Bytes |
| --- | --- | --- |
| `okid()` | 21 characters | 22 |
| `okid(length)` | `length` characters, from 8 to 128 | 1 + the string |
| `okid({ sortable: true })` | 8 time characters, then random. Length is at least 16. | 1 + the string |
| `okid({ prefix })` | The prefix, then the body. The prefix is at most 32 characters. | 1 + the string |

`length` is the body. The prefix is extra. `sortable` keeps time order under collation `C`. `lowercase`, `uppercase`, `numbers`, and `symbols` default to included. `lookAlikes: false` drops characters that are easy to confuse.

`schema({ defaults: { id: okid() } })` applies to every bare `t.id()`. A column option wins. See [id default](/docs/define/schema/defaults).

```ts title="users.ts"
import { okid } from "okmodel/ids";
import { table, t } from "okmodel/pg";

export const users = table("users", {
  id: t.id({ default: okid({ prefix: "usr_", sortable: true }) }), // [!code focus]
  email: t.text(),
});
```

### Ranges

A range is empty, or a pair of bounds. A null bound is unbounded. `lowerInclusive` and `upperInclusive` choose `[` or `(`.

| Builder | SQL | Bound | Bytes | Postgres |
| --- | --- | --- | --- | --- |
| `t.int4range()` | `int4range` | `number`, the integer range | variable | yes |
| `t.int8range()` | `int8range` | `string`, a bigint | variable | yes |
| `t.numrange()` | `numrange` | `string`, a numeric | variable | yes |
| `t.daterange()` | `daterange` | `Temporal.PlainDate` | variable | yes |
| `t.tstzrange()` | `tstzrange` | `Temporal.Instant` | variable | yes |

There is no `tsrange` builder. An instant range is `t.tstzrange()`.

```ts title="plans.ts"
import { table, t } from "okmodel/pg";

export const plans = table("plans", {
  id: t.identity(),
  scores: t.int4range(), // [!code focus]
  active: t.tstzrange(), // [!code focus]
});
```

A bound is `{ empty: false, lower, upper, lowerInclusive, upperInclusive }`. `{ empty: true }` is the empty range.

### Network

Values are canonical text.

| Builder | SQL | Value | Bytes | Postgres |
| --- | --- | --- | --- | --- |
| `t.inet()` | `inet` | An IPv4 or IPv6 host. A prefix is optional. | 7 or 19 | yes |
| `t.cidr()` | `cidr` | An IPv4 or IPv6 network. A prefix is required. | 7 or 19 | yes |
| `t.macaddr()` | `macaddr` | Six lowercase groups: `aa:bb:cc:dd:ee:ff`. | 6 | yes |
| `t.macaddr8()` | `macaddr8` | Eight lowercase groups. | 8 | yes |

7 bytes is IPv4. 19 bytes is IPv6.

An IPv4 prefix above 32, or any prefix above 128, is refused. `t.cidr()` with no prefix is refused.

```ts title="hosts.ts"
import { table, t } from "okmodel/pg";

export const hosts = table("hosts", {
  id: t.identity(),
  address: t.inet(), // [!code focus]
  block: t.cidr(), // [!code focus]
  mac: t.macaddr(), // [!code focus]
});
```

### Geometry

Built-in plane geometry. A point is `{ x, y }`. A line is `{ a, b, c }` for `ax + by + c = 0`.

| Builder | SQL | TypeScript | Bytes | Postgres |
| --- | --- | --- | --- | --- |
| `t.point()` | `point` | `{ x: number, y: number }` | 16 | yes |
| `t.line()` | `line` | `{ a: number, b: number, c: number }` | 24 | yes |

`box`, `circle`, `lseg`, `path`, and `polygon` have no builder. PostGIS is not these types.

```ts title="places.ts"
import { table, t } from "okmodel/pg";

export const places = table("places", {
  id: t.identity(),
  location: t.point(), // [!code focus]
});
```

### Search

| Builder | SQL | TypeScript | Bytes | Postgres |
| --- | --- | --- | --- | --- |
| `t.tsvector()` | `tsvector` | `string` | variable | yes |
| `t.ltree()` | `ltree` | `string` | variable | extension |

`t.tsvector()` stores the vector text. A generated vector is [`.generated()`](/docs/define/table/defaults). `t.ltree()` is labels separated by dots, such as `top.science`. Each label is letters, digits, or underscores. The schema lists `extension("ltree")` from `okmodel/ext`. See [extensions](/docs/define/schema/extensions). There is no `tsquery` builder.

```ts title="notes.ts"
import { extension } from "okmodel/ext";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  search: t.tsvector(), // [!code focus]
  path: t.ltree(), // [!code focus]
});

export default schema({
  tables: [notes],
  extensions: [extension("ltree")], // [!code focus]
});
```

<Callout type="error" title="OKM1020">
`t.ltree()` with no `ltree` extension on the schema is refused when the catalog is built.
</Callout>

### Named types

| Builder | SQL | TypeScript | Bytes | Postgres |
| --- | --- | --- | --- | --- |
| `t.enum(name, values)` | the enum name | the union of the labels | 4 | yes |
| `t.domain(name, base, check)` | the domain name | the base column's type | the base type | yes |
| `t.custom({ sqlType, encode, decode })` | `sqlType` | the type `encode` accepts | the SQL type | yes |

An [enum](/docs/define/table/shape#enum) is its own type. A [picklist](/docs/define/table/shape#picklist) stays a string column and adds a CHECK.

`t.domain("email", t.text(), "VALUE LIKE '%@%'")` stores the check as written. The name and the check must be non-empty. An enum cannot be the base. Changing the base type of an existing domain is OKM1020.

`t.custom()` stores a SQL type that has no builder. `sqlType` must be non-empty. `accepts` names the object kinds the codec takes, such as `"Object"`. Omit `accepts` for a scalar. `extension` records an extension dependency. `sqlForm` defaults to `"quote"`.

```ts title="notes.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  email: t.domain("email", t.text(), "VALUE LIKE '%@%'"), // [!code focus]
  label: t.custom({
    sqlType: "text",
    encode: (value: string) => value,
    decode: (wire: string) => wire,
  }), // [!code focus]
});
```

### Other Postgres types

Postgres has these types. This dialect has no builder under that name. The Postgres cell stays `yes`, because the database has the type. The call is what you write instead.

| Postgres type | Bytes | Call | Postgres |
| --- | --- | --- | --- |
| `smallserial`, `serial`, `bigserial` | 2, 4, 8 | `t.identity()` | yes |
| `money` | 8 | `t.numeric(precision, scale)` | yes |
| `decimal` | variable | `t.numeric()` | yes |
| `tsrange` | variable | `t.tstzrange()` | yes |
| `tsquery` | variable | `t.custom()` with `sqlType: "tsquery"` | yes |
| `bit`, `bit varying` | 1 byte per 8 bits, plus 5 or 8 | `t.custom()` with that SQL type | yes |
| `xml`, `hstore` | variable | `t.custom()` with that SQL type | yes |
| `box`, `lseg` | 32 | `t.custom()` with that SQL type | yes |
| `circle` | 24 | `t.custom()` with that SQL type | yes |
| `path` | 16 + 16 per point | `t.custom()` with that SQL type | yes |
| `polygon` | 40 + 16 per point | `t.custom()` with that SQL type | yes |
| `oid` | 4 | `t.custom()` with that SQL type | yes |
| `pg_lsn` | 8 | `t.custom()` with that SQL type | yes |
| multirange | variable | `t.custom()` with that SQL type | yes |


# Defaults (/docs/define/table/defaults)

<Callout type="info" title="Where this fits">
A column with no default is required and not null. This page is how a missing value is filled, from nullable through a generated expression. [Shape](/docs/define/table/shape) is next, when one column holds more than one value.
</Callout>

Insert requires the field unless the column is nullable, has a default, or is left out of input. Pass a value and that value is stored. Update does not fill the default again. A later `.default()`, `.defaultSql()`, or `.generated()` replaces the one before it.

### Nullable

`.nullable()` allows SQL NULL. Omit the field, or pass `null`. The catalog stores no default.

```ts title="nullable.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  publishedAt: t.timestamptz().nullable(), // [!code focus]
});
```

### Literal

`.default(literal)` stores the encoded literal in the catalog. Omit the field on insert and Postgres stores that literal.

```ts title="literal.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text().default(""), // [!code focus]
});
const app = schema({ tables: [notes] });

export async function add(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert({}); // [!code focus]
}
```

<Callout type="error" title="OKM1060">
A default outside a [picklist](/docs/define/table/shape#picklist) is refused. The message names the value and the list.
</Callout>

### Generator

`.default(generator)` runs when insert omits the field. The catalog stores no default, so a writer that bypasses okmodel must supply the value. Pass a value and that value is stored instead.

```ts title="generator.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { uuidv7 } from "okmodel/ids";

const notes = table("notes", {
  id: t.uuid().default(uuidv7), // [!code focus]
  title: t.text(),
});
const app = schema({ tables: [notes] });

export async function add(url: string, title: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert({ title });
}
```

<Callout type="info" title="Generators">
A generator is `uuidv4`, `uuidv7`, or [OKID](/docs/define/table/columns#okid) (`okid(...)`), all from `okmodel/ids`, or any function. `connect({ generators })` replaces `uuidv4`, `uuidv7`, and `okid`. A function you wrote is left alone. These run in the application. They are not the database functions `uuidv7()` and `gen_random_uuid()`.
</Callout>

### Already filled

`t.id()` and `t.identity()` already carry a default. Both are omitted from insert and update, so input cannot set them. The schema [id default](/docs/define/schema/defaults) changes what a bare `t.id()` means. A column option wins.

| Call | What fills it | Insert |
| --- | --- | --- |
| `t.id()` | Database `gen_random_uuid()`, unless the schema default says otherwise. | Omitted. |
| `t.id({ default: "uuidv7" })` | Database `uuidv7()`. Needs Postgres 18. | Omitted. |
| `t.id({ default: "uuidv4" })` | Database `gen_random_uuid()`, the same as a bare call. | Omitted. |
| `t.id({ default: uuidv7 })` | The client generator. Nothing is stored in the catalog. | Omitted. |
| `t.id({ default: "none" })` | Nothing. | Required. Still omitted from update. |
| `t.identity()` | `generated always as identity`. | Omitted. |

`t.id({ default: "none" })` is the column option. It is not a schema default.

#### database

A bare `t.id()` is omitted from insert. Postgres fills `gen_random_uuid()`. A large write-heavy table can store `uuidv7()` instead: the ids are time-ordered and the index stays local. That function needs Postgres 18 and `minPostgres: 18`.

```ts title="id-insert.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.id(), // [!code focus]
  title: t.text(),
});
const app = schema({ tables: [notes] });

export async function add(url: string, title: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert({ title });
}
```

#### supplied

`"none"` is required on insert. Update still cannot set it.

```ts title="id-none.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.id({ default: "none" }), // [!code focus]
  title: t.text(),
});
const app = schema({ tables: [notes] });

export async function add(url: string, id: string, title: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert({ id, title }); // [!code focus]
}
```

### Null

`.default(null)` is accepted only after `.nullable()`. The check runs when `.default()` is called, so the order is `.nullable().default(null)`.

```ts title="null-default.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  closedAt: t.timestamptz().nullable().default(null), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
A null default on a column that is not nullable is refused. Call `.nullable()` first.
</Callout>

### SQL

`.defaultSql(expression)` stores the expression as written. Omit the field and Postgres evaluates it. An empty expression is refused.

```ts title="default-sql.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  createdAt: t.timestamptz().defaultSql("now()"), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
`defaultSql` must be a non-empty SQL expression.
</Callout>

### Generated

`.generated(expression)` is `generated always as (expression) stored`. Insert and update omit the column. A read returns the value. An empty expression is refused. `{ stored: true }` is the default, and the SQL is stored either way.

```ts title="generated.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
  search: t.tsvector().generated("to_tsvector('simple', title)"), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
`generated()` must be a non-empty SQL expression.
</Callout>


# Filters (/docs/define/table/filters)

<Callout type="info" title="Where this fits">
The table is declared. This page is the allowlist a caller may send. [Queries](/docs/define/table/queries) are the calls that use the result.
</Callout>

`filters()` checks the allowlist when you call it. `parse` loads the parser the first time a request arrives and returns `{ where, orderBy }`. Spread that into `find`. `orderBy` is absent when the request has no `sort`. A request that is not an object is OKM1121: `filters parse expects an object of fields.`

A scalar equality is a plain value. Every other operator is a value `parse` builds. Copying that value through JSON drops it, and `find` throws OKM1121: `where must be an object of fields. Wrap an object value with eq.`

<Callout type="error" title="OKM1123">
A hidden field in `allow`, `sort`, or `relations` is refused when you call `filters()`. The message is `Field notes.secret is hidden.` `filterable` does not change that. See [hidden](/docs/define/table/visibility#hidden).
</Callout>

### Allow

`allow` maps a field to operators. A scalar, `null`, or an array is equality when that field lists `eq`. An object names operators. One operator stays that operator. Several operators on one field, or several fields, become `and`.

A field the request names, and that is not in `allow` or `relations`, is OKM1123: `Field rank is not in the allow list.` An own key such as `constructor` is a field name.

#### eq

A scalar, `null`, or an array is equality. An array here is the equality value, not `in`. The result is `{ where: { title: "ship" } }`.

```ts title="eq.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export async function named() {
  return notes.filters({ allow: { title: ["eq"] } }).parse({ title: "ship" }); // [!code focus]
}
```

#### in

`in` and `inList` both need an array, and both become `inList`. Listing `in` also accepts a request key `inList`. Listing only `inList` does not accept `in`.

```ts title="in.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  status: t.text(),
});

export async function statuses() {
  return notes.filters({ allow: { status: ["in"] } }).parse({
    status: { in: ["open", "closed"] }, // [!code focus]
  });
}
```

#### notIn

`notIn` needs an array. Anything else is OKM1120: `notIn needs an array.`

```ts title="not-in.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  status: t.text(),
});

export async function excluded() {
  return notes.filters({ allow: { status: ["notIn"] } }).parse({
    status: { notIn: ["draft"] }, // [!code focus]
  });
}
```

#### lt

`lt`, `lte`, `gt`, and `gte` each take the bound.

```ts title="lt.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export async function before() {
  return notes.filters({ allow: { title: ["lt"] } }).parse({
    title: { lt: "m" }, // [!code focus]
  });
}
```

#### between

`between` needs two bounds. One bound, or three, is OKM1120: `between on rank needs two bounds.`

```ts title="between.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  rank: t.integer(),
});

export async function ranked() {
  return notes.filters({ allow: { rank: ["between"] } }).parse({
    rank: { between: [1, 10] }, // [!code focus]
  });
}
```

#### startsWith

`startsWith`, `endsWith`, `like`, and `ilike` need a string. Anything else is OKM1120: `startsWith needs a string.`

```ts title="starts-with.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export async function prefixed() {
  return notes.filters({ allow: { title: ["startsWith"] } }).parse({
    title: { startsWith: "ship" }, // [!code focus]
  });
}
```

#### contains

`contains` takes the value you send. A string, a number, or a list is accepted here.

```ts title="contains.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export async function holding() {
  return notes.filters({ allow: { title: ["contains"] } }).parse({
    title: { contains: "ship" }, // [!code focus]
  });
}
```

#### not

`not` takes a scalar or `null`. An object is OKM1120: `not on title takes a scalar.`

```ts title="not.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export async function excluded() {
  return notes.filters({ allow: { title: ["not"] } }).parse({
    title: { not: "draft" }, // [!code focus]
  });
}
```

#### overlaps

`containedBy` and `overlaps` take the value you send. On an array column that value is a list.

```ts title="overlaps.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  tags: t.text().array(),
});

export async function tagged() {
  return notes.filters({ allow: { tags: ["overlaps", "containedBy"] } }).parse({
    tags: { overlaps: ["a"], containedBy: ["a", "b"] }, // [!code focus]
  });
}
```

#### hasKey

`hasKey` needs a string. `hasAnyKey` needs an array of strings. A number in that array is OKM1120: `hasAnyKey on meta needs an array of strings.`

```ts title="has-key.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  meta: t.jsonb(),
});

export async function marked() {
  return notes.filters({ allow: { meta: ["hasKey", "hasAnyKey"] } }).parse({
    meta: { hasKey: "published", hasAnyKey: ["published", "title"] }, // [!code focus]
  });
}
```

#### and

You do not list `and`. Two operators on one field, or two fields, become it.

```ts title="and.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
  rank: t.integer(),
});

export async function narrowed() {
  return notes
    .filters({ allow: { title: ["startsWith", "lt"], rank: ["eq"] } })
    .parse({ title: { startsWith: "a", lt: "m" }, rank: 1 }); // [!code focus]
}
```

<Callout type="error" title="OKM1120">
An operator the field does not list is `filters() cannot apply startsWith on title.` A scalar on a field that does not list `eq` is `filters() cannot apply eq on title.` An empty operator object is `filters() cannot apply a value on title.`
</Callout>

#### Refused

`path`, `matches`, `similar`, `wordSimilar`, boolean trees, relation quantifiers, and write operators. A caller cannot name them in `allow`. Naming one and calling `parse`, even with `{}`, is OKM1120: `filters() cannot apply path on title.`

### Sort

`sort` names the fields a caller may sort. The request key `sort` is one of those names, and `parse` orders that field ascending. There is no descending value. A name outside the list is OKM1123: `Field sort is not in the allow list.`

```ts title="sort.ts"
import { table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export async function ordered() {
  return notes.filters({ allow: { title: ["eq"] }, sort: ["title"] }).parse({
    title: "ship",
    sort: "title", // [!code focus]
  });
}
```

That result is `{ where: { title: "ship" }, orderBy: { title: "asc" } }`.

### Related fields

`relations` maps a [relation](/docs/define/table/relations) name to fields of that table. You name fields, not operators. Each field accepts every operator under [Allow](#allow). `parse` turns the object into `has`.

#### fields

The list is the fields a caller may filter on that relation. `filters()` refuses a hidden field on the related table. It does not check that the field is a column. `find` does, and a missing column is OKM1120: `Field name is not on users. Accepted names: …`.

```ts title="fields.ts"
import { one, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
    authorId: t.bigint().references("users"),
  },
  { relations: { author: one("users") } },
);

export async function byAuthor() {
  return notes.filters({ relations: { author: ["name"] } }).parse({
    author: { name: { startsWith: "a" } }, // [!code focus]
  });
}
```

A field outside that relation's list is OKM1123: `Field title is not in the allow list.` A scalar where the relation needs an object is OKM1120: `filters() cannot apply a value on author.`

#### column

A name in `allow` is the column when that name is also a relation. `{ list: "x" }` stays `{ where: { list: "x" } }`.

```ts title="column.ts"
import { many, table, t } from "okmodel/pg";

const owners = table(
  "owners",
  {
    id: t.identity(),
    list: t.text(),
  },
  { relations: { list: many("items", "ownerId") } },
);

export async function listed() {
  return owners.filters({ allow: { list: ["eq"] }, relations: { list: ["name"] } }).parse({
    list: "x", // [!code focus]
  });
}
```

### Request

Spread the result of `parse` into `find`.

#### simple

One field, and a sort. The result is `{ where: { title: "ship" }, orderBy: { title: "asc" } }`.

```ts title="simple.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({ tables: [notes] });

export async function named(url: string) {
  const parsed = await notes.filters({ allow: { title: ["eq"] }, sort: ["title"] }).parse({
    title: "ship", // [!code focus]
    sort: "title", // [!code focus]
  });
  await using db = connect(url, { schema: app });
  return db.notes.find({ ...parsed, limit: 20 }); // [!code focus]
}
```

#### complex

Several fields, a relation, and a sort. `parse` joins the fields with `and`. `where` is `and` of `not`, `startsWith`, `inList`, `between`, `overlaps`, `hasKey`, and `has`. `orderBy` is `{ rank: "asc" }`.

```ts title="complex.ts"
import { connect } from "okmodel/pg/postgresjs";
import { one, schema, table, t } from "okmodel/pg";

const users = table("users", {
  id: t.identity(),
  name: t.text(),
});

const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
    status: t.text(),
    rank: t.integer(),
    tags: t.text().array(),
    meta: t.jsonb(),
    authorId: t.bigint().references("users"),
  },
  { relations: { author: one("users") } },
);

const app = schema({ tables: [users, notes] });

export async function listed(url: string) {
  const parsed = await notes
    .filters({
      allow: {
        title: ["not", "startsWith"],
        status: ["in"],
        rank: ["between"],
        tags: ["overlaps"],
        meta: ["hasKey"],
      },
      sort: ["rank"],
      relations: { author: ["name"] },
    })
    .parse({
      title: { not: "draft", startsWith: "ship" }, // [!code focus]
      status: { in: ["open", "closed"] }, // [!code focus]
      rank: { between: [1, 10] }, // [!code focus]
      tags: { overlaps: ["a"] }, // [!code focus]
      meta: { hasKey: "published" }, // [!code focus]
      author: { name: { startsWith: "a" } }, // [!code focus]
      sort: "rank", // [!code focus]
    });
  await using db = connect(url, { schema: app });
  return db.notes.find({ ...parsed, limit: 20 }); // [!code focus]
}
```


# Overview (/docs/define/table)

<Anatomy call="table" shape="args" exported="notes">
  <AnatomyPart slot="name" label="Name" code='"notes",' mark="name">
    An empty name is refused.
  </AnatomyPart>
  <AnatomyPart slot="columns" label="Columns">
    <AnatomyLine code="{" />
    <AnatomyLine code="id: t.identity()," mark="key" indent={2} />
    <AnatomyLine code="title: t.text()," mark="type" indent={2} />
    <AnatomyLine code="published: t.boolean().default(false)," mark="after the type" indent={2} />
    <AnatomyLine code="}," />
    The [key](/docs/define/table/keys) comes first. Each other field is a [data type](/docs/define/table/columns). Defaults, names, and checks come after the type.
  </AnatomyPart>
  <AnatomyPart slot="options" label="Options" code="{ … }," mark="options">
    Keys, indexes, checks, and the pages below. [`schema()`](/docs/define/schema) builds the catalog.
  </AnatomyPart>
</Anatomy>

<Callout type="info" title="One builder">
`t` is every builder. Import one builder when a file uses only that type.
</Callout>

## The declaration

The list is how often you reach for each part while building. Columns, keys, and the calls come first.

```ts title="notes.ts"
import { index, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(), // [!code focus]
    title: t.text(),
    published: t.boolean().default(false),
  },
  { indexes: (columns) => [index(columns.title)] }, // [!code focus]
);
```

| Order | Page | You use it for |
| --- | --- | --- |
| 1 | [Columns](/docs/define/table/columns) | The builder for each value. |
| 2 | [Keys](/docs/define/table/keys) | The primary key, and a unique. |
| 3 | [Defaults](/docs/define/table/defaults) | What fills a column you omit. |
| 4 | [References](/docs/define/table/references) | A foreign key. |
| 5 | [Relations](/docs/define/table/relations) | The edge you query. |
| 6 | [Queries](/docs/define/table/queries) | The calls on the table. |
| 7 | [Traits](/docs/define/table/traits) | Shared columns, including timestamps and archive. |
| 8 | [Indexes](/docs/define/table/indexes) | An extra index. |
| 9 | [Validation](/docs/define/table/validation) | The rule that rejects a bad value. |
| 10 | [Shape](/docs/define/table/shape) | An array, a picklist, or an enum. |
| 11 | [Filters](/docs/define/table/filters) | What a caller may send. |
| 12 | [Presets](/docs/define/table/presets) | A named `where`. |
| 13 | [Tenancy](/docs/define/table/tenancy) | The schema key, global, or a path. |
| 14 | [Names](/docs/define/table/names) | The SQL name, a previous name, and a comment. |
| 15 | [Visibility](/docs/define/table/visibility) | Hidden, sensitive, and guarded. |
| 16 | [Checks](/docs/define/table/checks) | SQL the row must pass. |
| 17 | [Required rows](/docs/define/table/reference-rows) | Rows the application expects, inserted when the key is missing. |


# Indexes (/docs/define/table/indexes)

<Callout type="info" title="Where this fits">
The columns exist. This page adds an index the planner can use. A unique constraint stays on [Keys](/docs/define/table/keys). A rule the row must pass is a [check](/docs/define/table/checks).
</Callout>

`indexes` is a function. It receives one handle per column and returns an array of `index()` calls. `schema()` stores those indexes. A return value that is not an array is OKM1060: `Table notes indexes must return an array.`

<Callout type="info" title="SQL name">
The handle's `name` is the SQL name `schema()` will store. A [casing](/docs/define/schema/casing) of `"snake"` has already been applied by the time the catalog is written.
</Callout>

<Callout type="error" title="OKM1060">
An empty call is `Index on notes needs a column.` A value that is not a handle is `Index on notes must be built from column handles.` A return value that is not an array is `Table notes indexes must return an array.`
</Callout>

### Index

`index(...)` takes column handles, in index order. It needs at least one. You do not pass a name, a predicate, or a `using` method. Each argument is `columns.field`. The first argument is the first column of the index. The index is named `{table}_{columns}_idx`, using the SQL column names joined by `_`.

#### one

One column is one index. This one is `notes_title_idx`.

```ts title="one-column.ts"
import { index, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  { indexes: (columns) => [index(columns.title)] }, // [!code focus]
);
```

#### order

Column order is the index order. These two calls are different indexes: `notes_published_title_idx` and `notes_title_published_idx`.

```ts title="order.ts"
import { index, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
    published: t.boolean().default(false),
  },
  {
    indexes: (columns) => [
      index(columns.published, columns.title), // [!code focus]
      index(columns.title, columns.published), // [!code focus]
    ],
  },
);
```

#### several

Several indexes are several calls in the same array. A lookup on `title` and a lookup on `published` then `title` are stored separately.

```ts title="several.ts"
import { index, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
    published: t.boolean().default(false),
  },
  {
    indexes: (columns) => [
      index(columns.title), // [!code focus]
      index(columns.published, columns.title), // [!code focus]
    ],
  },
);
```

#### sql name

A [SQL name](/docs/define/table/names#sql-name) you set is the name that is joined. Here the table is `note` and the column is `published_at`, so the index is `note_published_at_idx`.

```ts title="sql-name.ts"
import { index, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    publishedAt: t.boolean().sqlName("published_at"), // [!code focus]
  },
  {
    sqlName: "note", // [!code focus]
    indexes: (columns) => [index(columns.publishedAt)],
  },
);
```

#### casing

[Casing](/docs/define/schema/casing) `"snake"` rewrites a name you did not set. `publishedAt` is stored as `published_at`, and the index is `notes_published_at_idx`.

```ts title="casing.ts"
import { index, schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  {
    id: t.identity(),
    publishedAt: t.boolean(), // [!code focus]
  },
  { indexes: (columns) => [index(columns.publishedAt)] },
);

export const app = schema({
  tables: [notes],
  casing: "snake", // [!code focus]
});
```

A new table adds the index with `create index`. A table that already exists adds it with `create index concurrently`. That step is not inside a transaction.

### Unique

`.unique()` marks that index unique. The columns and their order stay the ones you passed to `index()`.

#### one

One column is the usual case. This index is `users_email_idx`.

```ts title="unique-column.ts"
import { index, table, t } from "okmodel/pg";

export const users = table(
  "users",
  {
    id: t.identity(),
    email: t.text(),
  },
  { indexes: (columns) => [index(columns.email).unique()] }, // [!code focus]
);
```

#### several

Several columns are unique together, in the order you pass. This index is `notes_published_title_idx`.

```ts title="unique-columns.ts"
import { index, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
    published: t.boolean().default(false),
  },
  { indexes: (columns) => [index(columns.published, columns.title).unique()] }, // [!code focus]
);
```

#### beside

A plain index and a unique index can share the array. `title` is `notes_title_idx`. `email` is `notes_email_idx`, and only that one is unique.

```ts title="mixed.ts"
import { index, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
    email: t.text(),
  },
  {
    indexes: (columns) => [
      index(columns.title), // [!code focus]
      index(columns.email).unique(), // [!code focus]
    ],
  },
);
```

<Callout type="warning" title="Not a constraint">
A unique constraint and a unique index are different objects. A constraint belongs on [keys](/docs/define/table/keys). A foreign key waits for that constraint. `.unique()` here does not create it.
</Callout>


# Keys (/docs/define/table/keys)

<Callout type="info" title="Where this fits">
Start here. [Columns](/docs/define/table/columns) are the other fields.
</Callout>

One table has one primary key. A unique is a separate constraint.

### One column

`.primaryKey()` marks that column. Update omits it. Insert still accepts the value, unless the column is generated or guarded.

```ts title="notes.ts"
import { table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.uuid().primaryKey(), // [!code focus]
    title: t.text(),
  },
);
```

<Callout type="warning" title="Not a composite">
A second `.primaryKey()` is not a composite key. Use [several columns](#several-columns).
</Callout>

### Several columns

`primaryKey` lists the columns, in column order.

```ts title="memberships.ts"
import { table, t } from "okmodel/pg";

export const memberships = table(
  "memberships",
  {
    organizationId: t.uuid(),
    userId: t.uuid(),
  },
  { primaryKey: ["organizationId", "userId"] }, // [!code focus]
);
```

### Unique

A unique is its own constraint. The primary key stays separate.

| Form | Result |
| --- | --- |
| `.unique()` | One column. |
| `unique: { name: [fields] }` | Those fields together, in that order. `name` is the key you choose. |
| `.unique({ global: "reason" })` | On a tenant table, the tenant key is left out. |

#### column

`.unique()` is the constraint for one field.

```ts title="users.ts"
import { table, t } from "okmodel/pg";

export const users = table("users", {
  id: t.identity(),
  email: t.text().unique(), // [!code focus]
});
```

#### several

The table option names the constraint and lists the fields, in column order.

```ts title="logins.ts"
import { table, t } from "okmodel/pg";

export const logins = table(
  "logins",
  {
    id: t.identity(),
    email: t.text(),
    provider: t.text(),
  },
  { unique: { login: ["email", "provider"] } }, // [!code focus]
);
```

#### global

On a tenant table a unique includes the tenant key. `{ global: "reason" }` leaves that key out. `{ global: true, reason }` is the same opt-out.

```ts title="global.ts"
import { schema, table, t } from "okmodel/pg";
import { columnTenancy } from "okmodel/tenancy";

const users = table("users", {
  id: t.identity(),
  email: t.text().unique({ global: "one login email" }), // [!code focus]
});

export const app = schema({
  tables: [users],
  tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
});
```

<Callout type="error" title="OKM1060">
An empty reason is `unique() global needs a reason.` `{ global: true }` with no reason is `Unique users.email sets global and needs a reason.`
</Callout>

### id()

Needs PostgreSQL 18 or higher.

`t.id()` is a uuid primary key. The schema [id default](/docs/define/schema/defaults) is what a bare call means. A column option wins. [OKID](/docs/define/table/columns#okid) is `t.id({ default: okid() })`, stored as `text`.

| Call | What fills it | Insert |
| --- | --- | --- |
| `t.id()` | Database `gen_random_uuid()`, unless the schema default says otherwise. | Omitted. |
| `t.id({ default: "uuidv7" })` | Database `uuidv7()`. Needs Postgres 18. | Omitted. |
| `t.id({ default: "uuidv4" })` | Database `gen_random_uuid()`, the same as a bare call. | Omitted. |
| `t.id({ default: uuidv7 })` | The client generator. Nothing is stored in the catalog. | Omitted. |
| `t.id({ default: "none" })` | Nothing. | Required. Still omitted from update. |
| `t.identity()` | `generated always as identity`. A bigint. | Omitted. |

#### database

A bare `t.id()` stores `gen_random_uuid()`. Random ids are fine for most tables. A large write-heavy table can use `t.id({ default: "uuidv7" })`: the ids are time-ordered and the index stays local. That function needs Postgres 18 and `minPostgres: 18`.

```ts title="id.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.id(), // [!code focus]
  title: t.text(),
});
```

#### uuidv4

`"uuidv4"` stores `gen_random_uuid()`.

```ts title="uuidv4.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.id({ default: "uuidv4" }), // [!code focus]
  title: t.text(),
});
```

#### generator

A function from `okmodel/ids` runs on insert. The catalog stores no default.

```ts title="generator.ts"
import { uuidv7 } from "okmodel/ids";
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.id({ default: uuidv7 }), // [!code focus]
  title: t.text(),
});
```

#### none

`"none"` takes the id on insert. Update still omits it.

```ts title="supplied.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.id({ default: "none" }), // [!code focus]
  title: t.text(),
});
```

#### identity

`t.identity()` is a generated bigint, not a uuid.

```ts title="identity.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(), // [!code focus]
  title: t.text(),
});
```

<Callout type="error" title="OKM1060">
A default other than `uuidv7`, `uuidv4`, `none`, or a client generator is refused.
</Callout>


# Names (/docs/define/table/names)

<Callout type="info" title="Where this fits">
TypeScript sees the field name. This page is the SQL name, a rename that keeps the old column, and the comment. [Visibility](/docs/define/table/visibility) decides who can read or write it.
</Callout>

Insert, update, and `where` use the field name. The SQL name is what the statement quotes.

### SQL name

The SQL name is the field name, unless you replace it. [Casing](/docs/define/schema/casing) `"snake"` rewrites a name you did not set. `publishedAt` is `published_at`. `userID` is `user_i_d`. Casing does not rewrite a name you set.

| Call | Result |
| --- | --- |
| `.sqlName("headline")` | That column is stored as `headline`. |
| `sqlName: "note"` | The table is stored as `note`. |

#### column

`.sqlName()` replaces one column. Insert still uses the field name.

```ts title="column.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text().sqlName("headline"), // [!code focus]
});
const app = schema({ tables: [notes] });

export async function add(url: string, title: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert({ title });
}
```

#### table

`sqlName` on the table replaces the table name. The client property stays `notes`.

```ts title="table.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { sqlName: "note" }, // [!code focus]
);
const app = schema({ tables: [notes] });

export async function add(url: string, title: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert({ title });
}
```

<Callout type="error" title="OKM1060">
An empty column or table `sqlName` is refused.
</Callout>

<Callout type="error" title="OKM1122">
A reserved word, a control character, or a name longer than 63 bytes is refused.
</Callout>

<Callout type="error" title="OKM1023">
Two tables that store the same SQL name are refused. The message lists the table names.
</Callout>

### Previous name

A migration plans `ALTER TABLE … RENAME TO` or `RENAME COLUMN`. Dropping one name and adding another, with no `renamedFrom`, is refused and the fix names the line to add. A previous name that the last snapshot does not contain is refused. Remove `renamedFrom` once the migration that used it has been applied. With no snapshot, that check does not run.

| Call | Result |
| --- | --- |
| `.renamedFrom("headline")` | The previous column SQL name. |
| `renamedFrom: "posts"` | The previous table SQL name. |

#### column

`.renamedFrom()` is the previous column.

```ts title="column-rename.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  title: t.text().renamedFrom("headline"), // [!code focus]
});
```

#### table

`renamedFrom` on the table is the previous table name.

```ts title="table-rename.ts"
import { table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { renamedFrom: "posts" }, // [!code focus]
);
```

<Callout type="error" title="OKM1060">
An empty column or table `renamedFrom` is refused.
</Callout>

<Callout type="error" title="OKM1020">
`Table renamedFrom … is not in the previous snapshot.` A column message names the table and the previous column.
</Callout>

<Callout type="error" title="OKM1530">
`Drop headline and add title could be a rename.` The fix is `.renamedFrom("headline")` on the new column, or `renamedFrom: "posts"` on the new table.
</Callout>

### Comment

`.comment()` and a table `comment` are checked. The catalog has no comment field, so neither is emitted. Omit the call to leave it off. An empty table comment is refused. An empty column comment is accepted.

#### column

`.comment()` is checked on that column and not stored.

```ts title="column-comment.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  title: t.text().comment("Shown in the list"), // [!code focus]
});
```

#### table

A table `comment` is checked and not stored.

```ts title="table-comment.ts"
import { table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { comment: "One row per note" }, // [!code focus]
);
```

<Callout type="error" title="OKM1060">
An empty table `comment` is refused.
</Callout>

<Callout type="error" title="OKM1122">
A comment that contains a control character is refused.
</Callout>


# Presets (/docs/define/table/presets)

<Callout type="info" title="Where this fits">
The table is declared. A preset names a `where` you would otherwise repeat, such as `notes.pending()`.
</Callout>

A preset receives a builder and returns that builder. `where` is the only method. It adds a predicate. It cannot remove or replace one. The caller's `where` stays first. Every added predicate joins it with AND. The tenant predicate and an active set are not in the builder. The planner puts them around the preset.

Return the builder you were given. Anything else is OKM1121: `Preset pending on notes must return the builder it was given.`

`inspect()` records one line per call, such as `pending filters status`, from `table notes` or `trait flagged`. A preset that adds no `where` records `filters nothing`.

### where

#### field

A fixed value. `notes.pending()` keeps `status` equal to `"pending"`.

```ts title="field.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), title: t.text(), status: t.text() },
  { presets: { pending: (q) => q.where({ status: "pending" }) } }, // [!code focus]
);

const app = schema({ tables: [notes] });

export async function pending(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.pending().find({ limit: 20 }); // [!code focus]
}
```

#### argument

The arguments after the builder are the method's arguments.

```ts title="argument.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), title: t.text(), ownerId: t.uuid() },
  {
    presets: {
      ownedBy: (q, owner: string) => q.where({ ownerId: owner }), // [!code focus]
    },
  },
);

const app = schema({ tables: [notes] });

export async function mine(url: string, owner: string) {
  await using db = connect(url, { schema: app });
  return db.notes.ownedBy(owner).find({ limit: 20 }); // [!code focus]
}
```

#### list

A rest argument passes a list. `inList` comes from `okmodel/pg`.

```ts title="list.ts"
import { connect } from "okmodel/pg/postgresjs";
import { inList, schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), status: t.text() },
  {
    presets: {
      inState: (q, ...states: readonly string[]) => q.where({ status: inList(states) }), // [!code focus]
    },
  },
);

const app = schema({ tables: [notes] });

export async function openOrPending(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.inState("open", "pending").find({ limit: 20 }); // [!code focus]
}
```

#### bound

`gte` is a predicate, the same as a fixed value.

```ts title="bound.ts"
import { connect } from "okmodel/pg/postgresjs";
import { gte, schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), priority: t.integer() },
  { presets: { urgent: (q) => q.where({ priority: gte(3) }) } }, // [!code focus]
);

const app = schema({ tables: [notes] });

export async function urgent(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.urgent().find({ limit: 20 }); // [!code focus]
}
```

#### or

`or` takes an array of filters. `or(a, b)` is OKM1121: `Pass an array: or([a, b]).`

```ts title="or.ts"
import { connect } from "okmodel/pg/postgresjs";
import { or, schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), status: t.text(), ownerId: t.uuid() },
  {
    presets: {
      mineOrOpen: (q, owner: string) =>
        q.where(or([{ ownerId: owner }, { status: "open" }])), // [!code focus]
    },
  },
);

const app = schema({ tables: [notes] });

export async function mineOrOpen(url: string, owner: string) {
  await using db = connect(url, { schema: app });
  return db.notes.mineOrOpen(owner).find({ limit: 20 }); // [!code focus]
}
```

#### several

Each `where` adds. The first predicate stays.

```ts title="several.ts"
import { connect } from "okmodel/pg/postgresjs";
import { gte, schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), status: t.text(), priority: t.integer() },
  {
    presets: {
      stale: (q) => q.where({ status: "pending" }).where({ priority: gte(2) }), // [!code focus]
    },
  },
);

const app = schema({ tables: [notes] });

export async function stale(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.stale().find({ limit: 20 }); // [!code focus]
}
```

### Calls

A preset filters `find`, `one`, `count`, `exists`, `page`, and `aggregate`. It also filters `update`, `delete`, `archive`, and `restore`. Insert does not use it.

A call does not replace the handle it started from. `const pending = db.notes.pending()` still means pending after `pending.ownedBy(owner)`.

Update and delete still need their own `where`. A preset does not stand in for one. `notes.pending().delete({})` is OKM1102: `delete on notes needs a where. Pass a filter, or .all(reason).` `.all("clear the backlog")` matches every pending row.

#### simple

One preset, then `find`.

```ts title="simple.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), title: t.text(), status: t.text() },
  { presets: { pending: (q) => q.where({ status: "pending" }) } },
);

const app = schema({ tables: [notes] });

export async function pending(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.pending().find({ limit: 20 }); // [!code focus]
}
```

#### complex

The chain is `pending`, then `ownedBy`, then `urgent`. The caller's `where` stays first. Update uses the same `pending` filter beside its own `where`.

```ts title="complex.ts"
import { connect } from "okmodel/pg/postgresjs";
import { gte, schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
    status: t.text(),
    ownerId: t.uuid(),
    priority: t.integer(),
  },
  {
    presets: {
      pending: (q) => q.where({ status: "pending" }),
      ownedBy: (q, owner: string) => q.where({ ownerId: owner }),
      urgent: (q) => q.where({ priority: gte(3) }),
    },
  },
);

const app = schema({ tables: [notes] });

export async function review(url: string, owner: string, id: string) {
  await using db = connect(url, { schema: app });
  const rows = await db.notes
    .pending()
    .ownedBy(owner)
    .urgent()
    .find({ where: { title: "ship" }, limit: 20 }); // [!code focus]
  await db.notes.pending().update({ where: { id }, set: { title: "Done" } }); // [!code focus]
  return rows;
}
```

### Names

A reserved name is a type error. On a trait, `trait()` throws OKM1040 at the call. On a table, `connect` throws it before the first query: `Preset find on table notes is a client method or a reserved name. Reserved: find, one, page, aggregate, count, exists, insert, update, delete, descendants, ancestors, sync, archive, restore, withArchived, onlyArchived, then, lock, watch, subscribe, stream, inspect, explain, with, for, as.`

### Traits

A trait can add presets. They see the trait's columns. The table's own names are already in the map, so a trait that repeats one is OKM1040: `Preset pending on notes is defined by table notes and by trait flagged.` Two traits that share a name are `Preset pending on notes is defined by trait flagged and by trait other.` Schema traits apply first. See [traits](/docs/define/table/traits).

```ts title="trait.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { trait } from "okmodel/traits";

const flagged = trait("flagged", {
  fields: { flagged: t.boolean().default(false) },
  presets: { flagged: (q) => q.where({ flagged: true }) }, // [!code focus]
});

const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { traits: [flagged] },
);

const app = schema({ tables: [notes] });

export async function flaggedNotes(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.flagged().find({ limit: 20 }); // [!code focus]
}
```


# Queries (/docs/define/table/queries)

<Callout type="info" title="Where this fits">
The table is declared. This page is each call, in the order you meet it: the first rows of a relation, a window, search, a tree, a stream, then the writes.
</Callout>

`one` returns the row or `null`. More than one match is `not_unique`: `one() on notes matched more than one row. Pass orderBy to take the first, or narrow where.` [Basic Usage](/docs/start/basic-usage) is insert, find, one, count, exists, update, and delete. A caller-supplied filter goes through [filters](/docs/define/table/filters).

### Page

`page` returns `{ items, next }`. `next` is `null` on the last page. Pass it back as `after` with the same `orderBy`. The cursor is the order plus the primary key, so a page stays stable while rows are inserted.

| Input | Result |
| --- | --- |
| `limit` | Required. The page size. |
| omitted `after` | The first page. |
| `after` | The cursor from `next`. |
| `next` | `null` on the last page. |

#### first

`limit` is required. Omit `after` for the first page.

```ts title="first.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", { id: t.identity(), title: t.text() });
const app = schema({ tables: [notes] });

export async function first(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.page({ orderBy: { title: "asc" }, limit: 20 }); // [!code focus]
}
```

#### next

Pass `next` back as `after`, with the same `orderBy`.

```ts title="page.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", { id: t.identity(), title: t.text() });
const app = schema({ tables: [notes] });

export async function next(url: string, after: string | null) {
  await using db = connect(url, { schema: app });
  return db.notes.page({ orderBy: { title: "asc" }, limit: 20, after }); // [!code focus]
}
```

<Callout type="error" title="OKM1101">
Omitting `limit` is `page() needs a limit.`
</Callout>

<Callout type="error" title="OKM1130">
A cursor this call did not return is `after is not a cursor that page() returned.`
</Callout>

### Aggregate

`aggregate` counts, sums, averages, and finds extremes. There is no `having`.

| Input | Result |
| --- | --- |
| no `groupBy` | One row. |
| `groupBy` | One row per group. Needs `limit`, or `.all(reason)`. |
| `sum`, `avg` | A list of field names. |
| `count: true` | The row count. |

#### count

With no `groupBy` the result is one row.

```ts title="count.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const tasks = table("tasks", {
  id: t.identity(),
  status: t.text(),
  amount: t.integer(),
});
const app = schema({ tables: [tasks] });

export async function totals(url: string) {
  await using db = connect(url, { schema: app });
  return db.tasks.aggregate({ count: true }); // [!code focus]
}
```

#### grouped

`groupBy` needs `limit`, or `.all(reason)`. `sum` is a list of field names.

```ts title="aggregate.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const tasks = table("tasks", {
  id: t.identity(),
  status: t.text(),
  amount: t.integer(),
});
const app = schema({ tables: [tasks] });

export async function byStatus(url: string) {
  await using db = connect(url, { schema: app });
  return db.tasks.aggregate({
    groupBy: ["status"],
    count: true,
    sum: ["amount"], // [!code focus]
    limit: 20,
  });
}
```

<Callout type="error" title="OKM1101">
`aggregate() with groupBy needs a limit, or .all(reason) for every group.`
</Callout>

### Include

`limit` on a to-many `include` is the first N related rows of each parent.

| Option | Result |
| --- | --- |
| `limit` | Per parent, inside one lateral join. |
| `orderBy` | Order inside that parent. The primary key follows, ascending, for any key you did not name. |
| omitted `orderBy` | The primary key. |
| no related rows | `[]`. |
| `select`, a nested include, a preset | Apply. A `select` that names a hidden column drops that column and does not error. |
| `matches`, `relevance` | Apply inside the include. |
| `search` | Stays on `find` and `count`. |
| a window in `select` | Not in this version. |

#### simple

`limit` is the first related rows of each parent.

```ts title="include-limit.ts"
import { connect } from "okmodel/pg/postgresjs";
import { many, schema, table, t } from "okmodel/pg";

const posts = table(
  "posts",
  { id: t.identity(), title: t.text() },
  { relations: { comments: many("comments", "postId") } },
);
const comments = table("comments", {
  id: t.identity(),
  postId: t.bigint().references("posts"),
  body: t.text(),
});
const app = schema({ tables: [posts, comments] });

export async function thread(url: string) {
  await using db = connect(url, { schema: app });
  return db.posts.find({
    limit: 20,
    include: { comments: { limit: 3 } }, // [!code focus]
  });
}
```

#### complex

`orderBy` sorts inside that parent. `select` names the fields.

```ts title="include.ts"
import { connect } from "okmodel/pg/postgresjs";
import { many, schema, table, t } from "okmodel/pg";

const posts = table(
  "posts",
  { id: t.identity(), title: t.text() },
  { relations: { comments: many("comments", "postId") } },
);
const comments = table("comments", {
  id: t.identity(),
  postId: t.bigint().references("posts"),
  body: t.text(),
  score: t.integer(),
});
const app = schema({ tables: [posts, comments] });

export async function latest(url: string) {
  await using db = connect(url, { schema: app });
  return db.posts.find({
    limit: 20,
    include: {
      comments: { orderBy: { score: "desc" }, limit: 3, select: ["id", "body"] }, // [!code focus]
    },
  });
}
```

### Windows

`rowNumber`, `rank`, `denseRank`, and `runningTotal` go in `select` on `find` or `one`.

| Helper | Result |
| --- | --- |
| `rowNumber` | One number per row. The primary key is appended, so a tie stays stable. |
| `rank` | Equal values share a rank. The next rank skips. |
| `denseRank` | Equal values share a rank. The next rank is the next integer. |
| `runningTotal` | The column's type, or null. An integer is a number or null. A column that reads as text, including `numeric`, is that string or null. |

| Option | Result |
| --- | --- |
| `partitionBy` | The group. Omit it to number the whole result. |
| `orderBy` | Required. The order inside the group. |
| `as` | The name on the row. A column name is refused. |

#### simple

`rowNumber` numbers the rows in `orderBy`. Omit `partitionBy` to number the whole result.

```ts title="row-number.ts"
import { connect } from "okmodel/pg/postgresjs";
import { rowNumber, schema, table, t } from "okmodel/pg";

const tasks = table("tasks", {
  id: t.identity(),
  status: t.text(),
  amount: t.integer(),
});
const app = schema({ tables: [tasks] });

export async function numbered(url: string) {
  await using db = connect(url, { schema: app });
  return db.tasks.find({
    select: ["id", rowNumber({ orderBy: { amount: "desc" }, as: "n" })], // [!code focus]
    limit: 20,
  });
}
```

#### complex

`rank` skips after a tie. `denseRank` does not. `runningTotal` adds the column.

```ts title="windows.ts"
import { connect } from "okmodel/pg/postgresjs";
import { denseRank, rank, rowNumber, runningTotal, schema, table, t } from "okmodel/pg";

const tasks = table("tasks", {
  id: t.identity(),
  status: t.text(),
  amount: t.integer(),
});
const app = schema({ tables: [tasks] });

export async function numbered(url: string) {
  await using db = connect(url, { schema: app });
  return db.tasks.find({
    select: [
      "id",
      "status",
      rowNumber({ partitionBy: ["status"], orderBy: { amount: "desc" }, as: "n" }), // [!code focus]
      rank({ partitionBy: ["status"], orderBy: { amount: "desc" }, as: "r" }),
      denseRank({ partitionBy: ["status"], orderBy: { amount: "desc" }, as: "d" }),
      runningTotal("amount", { partitionBy: ["status"], orderBy: { id: "asc" }, as: "soFar" }),
    ],
    limit: 20,
  });
}
```

<Callout type="error" title="OKM1120">
`page()` reads `select` as field names, so a window there is OKM1120: `select must be a list of field names.` An `as` that is a column name is `rowNumber as id clashes with a column of tasks.` An empty `as` is `rowNumber needs as, the name of the result key.` A control character is `rowNumber as has a control character.` A name on `Object.prototype`, or `then`, is `rowNumber as __proto__ is a prototype key. Use another name for the result.` `orderBy` is required: `rowNumber needs orderBy, the order inside the group.` `lag`, `lead`, and a frame are not in this version.
</Callout>

<Callout type="error" title="OKM1124">
`runningTotal` on a column that is not a number or a string is refused.
</Callout>

### SQL fragment

`fragment` is a SQL expression you can nest. A string, number, boolean, or null is a parameter. `ident` names the field, the same name `where` uses, and a hidden field is refused (OKM1120); `hidden({ filterable: true })` is allowed in `where` only. `fragment.column` is the field the fragment is attached to in `where`. `.as(name)` puts a numeric expression in `select` on `find` or `one`. The tenant predicate stays in front of the fragment.

The check `sql` tag is a different function. It builds a check, an index predicate, or a default. It does not accept parameters.

#### where and select

`length(${fragment.column})` uses the `title` field this fragment filters. `ident("status")` is the field `status`. `len` is a number.

```ts title="fragment.ts"
import { connect } from "okmodel/pg/postgresjs";
import { fragment, ident, schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
  status: t.text(),
});
const app = schema({ tables: [notes] });

const open = fragment`${ident("status")} = ${"open"}`;

export async function openNotes(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.find({
    where: { title: fragment`length(${fragment.column}) > ${3} and ${open}` }, // [!code focus]
    select: ["id", fragment`length(${ident("title")})`.as("len")], // [!code focus]
    limit: 20,
  });
}
```

<Callout type="error" title="OKM1125">
A fragment that closes the surrounding predicate, a comment, a semicolon, a `$`, or a backslash in the literal is OKM1125. So is an interpolation inside quotes, an empty fragment, and a select fragment that is not a number. Pass a value that needs a backslash as an interpolation. `ident("")` is OKM1122.
</Callout>

### Text search

`find({ search })` and `count({ search })` match a [search column](/docs/define/table/columns#search). A string is enough when the table has one. Rows come back closest first, then by primary key, unless you pass `orderBy`. `count` uses the same match and does not order.

| `match` | Query |
| --- | --- |
| `natural` | `websearch_to_tsquery`. The default. Unquoted words are combined with AND. |
| `all-words` | `plainto_tsquery`. |
| `phrase` | `phraseto_tsquery`. |
| `advanced` | `to_tsquery`. Raw syntax. A syntax error in that query is OKM1120. Other syntax errors stay a driver error. |

`language` is a text search language such as `english`. Leave it out for the database default. A well-formed name that Postgres does not know as a language reaches the server and comes back as a driver error. When the table has more than one search column, name it with `in`. The error lists the names. `{ mode, config }` is not accepted. Ranking is `ts_rank`. `matches` and `relevance` take the same `{ match, language }`.

#### simple

A string is enough when the table has one search column. Rows come back closest first.

```ts title="simple-search.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const pages = table("pages", {
  id: t.identity(),
  title: t.text(),
  body: t.tsvector(),
});
const app = schema({ tables: [pages] });

export async function found(url: string) {
  await using db = connect(url, { schema: app });
  return db.pages.find({ search: "deploy rollback", limit: 20 }); // [!code focus]
}
```

#### complex

`match` and `language` name the query. `where` and `orderBy` use the same text, so the filter and the rank agree.

```ts title="search.ts"
import { connect } from "okmodel/pg/postgresjs";
import { matches, relevance, schema, table, t } from "okmodel/pg";

const pages = table("pages", {
  id: t.identity(),
  title: t.text(),
  body: t.tsvector(),
});
const app = schema({ tables: [pages] });

export async function ranked(url: string) {
  await using db = connect(url, { schema: app });
  return db.pages.find({
    search: { for: "deploy rollback", match: "phrase", language: "english" }, // [!code focus]
    where: { body: matches("deploy rollback", { match: "phrase", language: "english" }) },
    orderBy: {
      body: relevance("deploy rollback", { match: "phrase", language: "english" }),
      id: "asc",
    },
    limit: 20,
  });
}
```

<Callout type="error" title="OKM1120">
`page()`, `one`, and `exists` do not take `search`. A `language` that is not text is refused. A highlighted snippet and a search of a plain text column are not in this version.
</Callout>

<Callout type="error" title="OKM1122">
A missing text search configuration is OKM1122, including `public.english`. A malformed advanced search is OKM1120 and names the search input. An empty or malformed name is still OKM1122 when the statement is built.
</Callout>

### Tree

`descendants(id)` reads the rows below `id`. `ancestors(id)` reads the rows above it. Declare a [relation](/docs/define/table/relations) from the table to itself. `ltree` is not required.

| Option | Result |
| --- | --- |
| `relation` | Required when the self-relations use different columns. The fix lists the names. |
| `depth` | Stops after that many steps. `0` returns nothing unless `self: true`, which returns the start row. |
| `where` | Filters the rows that come back. It does not cut the walk short. |
| a missing id | No rows. |
| a cycle | A row already on the path is not entered again. |
| `include` | Not available. |

#### descendants

`descendants(id)` reads the rows below `id`. `depth` stops the walk.

```ts title="descendants.ts"
import { connect } from "okmodel/pg/postgresjs";
import { many, schema, table, t } from "okmodel/pg";

const nodes = table(
  "nodes",
  {
    id: t.identity(),
    parentId: t.bigint().nullable().references("nodes"),
    name: t.text(),
  },
  { relations: { children: many("nodes", "parentId") } },
);
const app = schema({ tables: [nodes] });

export async function below(url: string, id: string) {
  await using db = connect(url, { schema: app });
  return db.nodes.descendants(id, { depth: 2, select: ["id", "name"] }); // [!code focus]
}
```

#### ancestors

`ancestors(id)` reads the rows above `id`.

```ts title="ancestors.ts"
import { connect } from "okmodel/pg/postgresjs";
import { many, schema, table, t } from "okmodel/pg";

const nodes = table(
  "nodes",
  {
    id: t.identity(),
    parentId: t.bigint().nullable().references("nodes"),
    name: t.text(),
  },
  { relations: { children: many("nodes", "parentId") } },
);
const app = schema({ tables: [nodes] });

export async function above(url: string, id: string) {
  await using db = connect(url, { schema: app });
  return db.nodes.ancestors(id, { select: ["id", "name"] }); // [!code focus]
}
```

<Callout type="error" title="OKM1120">
No self-relation is `nodes has no relation to itself.` More than one, and no `relation`, is `nodes has more than one relation to itself. Pass relation.` A primary key that is not exactly one column besides the tenant key is `nodes needs one primary key column besides the tenant key.` `depth` must be an integer from 0 up.
</Callout>

### Sync

`sync(id, relation, ids)` makes a to-many relation hold exactly those ids. It locks the parent, then writes, and returns `{ added, removed }`. Duplicate ids count once. A join table gains and loses link rows. A nullable foreign key is set or cleared, and the related row stays. A link that would have to delete a row is refused.

A missing parent is `not_found`: `sync on nodes did not find that id.` An id that is not on the related table is `not_found`: `sync on nodes.children is missing 1 ids.` The relation must be `many` or `manyThrough`: `sync on nodes.children is not a to-many relation.` A tree-shaped `many` (the same table, through its own foreign key) refuses a list that contains the parent's own id. `manyThrough` accepts that link, including a row that follows itself. An `update` that sets a tree parent to the row's own id, or to a descendant, is OKM1120 when `where` is the primary key. A filter that is not the key is not checked.

#### simple

One call sets the relation to those ids. Duplicate ids count once.

```ts title="sync.ts"
import { connect } from "okmodel/pg/postgresjs";
import { many, schema, table, t } from "okmodel/pg";

const nodes = table(
  "nodes",
  {
    id: t.identity(),
    parentId: t.bigint().nullable().references("nodes"),
    name: t.text(),
  },
  { relations: { children: many("nodes", "parentId") } },
);
const app = schema({ tables: [nodes] });

export async function links(url: string, id: string, child: string) {
  await using db = connect(url, { schema: app });
  return db.nodes.sync(id, "children", [child]); // [!code focus]
}
```

#### complex

An empty list clears the links. The next call sets one id. Each call returns `{ added, removed }`.

```ts title="sync-clear.ts"
import { connect } from "okmodel/pg/postgresjs";
import { many, schema, table, t } from "okmodel/pg";

const nodes = table(
  "nodes",
  {
    id: t.identity(),
    parentId: t.bigint().nullable().references("nodes"),
    name: t.text(),
  },
  { relations: { children: many("nodes", "parentId") } },
);
const app = schema({ tables: [nodes] });

export async function replace(url: string, id: string, child: string) {
  await using db = connect(url, { schema: app });
  const cleared = await db.nodes.sync(id, "children", []); // [!code focus]
  const set = await db.nodes.sync(id, "children", [child]);
  return { cleared, set };
}
```

### Stream

`.stream()` walks `find` in keyset batches. It does not open a server cursor. Leaving the loop does not hold a connection.

| Option | Result |
| --- | --- |
| `batch` | An integer from 1 to 2147483647. The default is 1000. |
| `limit` | The total. `0` yields nothing and runs no query. |
| `.all(reason)` | The set, with no `limit`. Both together still honor `limit`. |
| `orderBy` | Then the primary key. Omit it and the rows follow the primary key. |
| `timeout` | The time for the whole stream. On a driver that cannot cancel, the check is between batches. |
| `signal` | Stops the stream. |

`select`, `include`, a preset, and the archive set work as they do on `page()`. A row committed while the stream runs can show up, or not. `tx({ isolation: "repeatable read" })` is one snapshot of the primary, and that transaction holds the connection. On a client with replicas, the first batch chooses the endpoint and later batches stay there.

#### limit

`limit` is the total. `batch` is how many rows each query reads.

```ts title="stream.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", { id: t.identity(), title: t.text() });
const app = schema({ tables: [notes] });

export async function titles(url: string): Promise<string[]> {
  await using db = connect(url, { schema: app });
  const out: string[] = [];
  for await (const row of db.notes.find({ limit: 20 }).stream({ batch: 20 })) { // [!code focus]
    out.push(row.title);
  }
  return out;
}
```

#### snapshot

`.all(reason)` is the whole set. `tx({ isolation: "repeatable read" })` is one snapshot, and that transaction holds the connection.

```ts title="stream-all.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", { id: t.identity(), title: t.text() });
const app = schema({ tables: [notes] });

export async function each(url: string): Promise<void> {
  await using db = connect(url, { schema: app });
  await db.tx({ isolation: "repeatable read" }, async (tx) => {
    for await (const row of tx.notes.find({}).all("export").stream({ batch: 500 })) { // [!code focus]
      if (row.title === "") break;
    }
  });
}
```

<Callout type="error" title="OKM1120">
A table with no primary key is refused. `one()`, `count()`, `exists()`, `search`, and a window are refused.
</Callout>

<Callout type="error" title="OKM1101">
A stream with no `limit` and no `.all(reason)` is refused.
</Callout>

<Callout type="error" title="OKM1121">
`batch` outside 1 to 2147483647 is refused. A `timeout` that is not a number above 0 is refused.
</Callout>

### Unknown keys

An unknown key on `insert`, `update`, upsert / `onConflict`, or first-or-create is OKM1120. The message names the key and the table. Typos are no longer dropped silently. Guarded, sealed, and computed fields keep their existing codes (OKM1190 and the rest).

<Callout type="error" title="OKM1120">
`Field nope is not on notes.` Use a field from the catalog.
</Callout>

### Supplied id

A client-generator `t.id({ default: uuidv4 })` omits `id` from insert. `{ allow: ["id"] }` writes the id you pass and skips the generator for that row. A list can mix a supplied id with a row that omits it. Without `allow`, a supplied id is OKM1190. On a list, `allow` has to be exactly `["id"]`. A longer list does not open `id` in the type.

`t.id()` and `t.id({ default: "uuidv7" })` are database defaults. The type accepts `allow: ["id"]` on that id, and the runtime fails with OKM1190. Update does not change the key. A primary key with no client generator, including `.primaryKey().guarded()`, stays refused. A guarded field that is not the key still opens with `allow`.

```ts title="supplied-id.ts"
import { uuidv4 } from "okmodel/ids";
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const users = table("users", { id: t.id({ default: uuidv4 }), name: t.text() });
const app = schema({ tables: [users] });

export async function keep(url: string, id: string) {
  await using db = connect(url, { schema: app });
  return db.users.insert(
    [{ id, name: "Ada" }, { name: "Grace" }],
    { allow: ["id"] },
  );
}
```

<Callout type="error" title="OKM1190">
A supplied id without `allow`, a database-default id named in `allow`, and an update of the key are refused.
</Callout>

### First or create

`onConflict` decides what an insert does when a unique is already taken.

| Value | Result |
| --- | --- |
| omitted, or `"error"` | The insert fails. |
| `"ignore"` | The new row is skipped. |
| `{ on, update }` | Those columns are set from the new row. |
| `{ on, return: true }` | The existing row is read and not changed. |

`on` is a unique or the primary key. `update` and `return` together are refused.

#### return

`{ on, return: true }` inserts the row. When that unique is taken, it reads the existing row and does not change it. The lookup runs only when the insert returned nothing.

```ts title="create.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const tags = table("tags", { id: t.identity(), name: t.text().unique() });
const app = schema({ tables: [tags] });

export async function once(url: string) {
  await using db = connect(url, { schema: app });
  return db.tags.insert({ name: "ship" }, { onConflict: { on: "name", return: true } }); // [!code focus]
}
```

#### ignore

`"ignore"` skips a row whose unique is taken.

```ts title="ignore.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const tags = table("tags", { id: t.identity(), name: t.text().unique() });
const app = schema({ tables: [tags] });

export async function keep(url: string) {
  await using db = connect(url, { schema: app });
  return db.tags.insert({ name: "ship" }, { onConflict: "ignore" }); // [!code focus]
}
```

#### update

`update` names the columns copied from the rejected row.

```ts title="upsert.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const tags = table("tags", { id: t.identity(), name: t.text().unique() });
const app = schema({ tables: [tags] });

export async function rename(url: string) {
  await using db = connect(url, { schema: app });
  return db.tags.insert({ name: "ship" }, { onConflict: { on: "name", update: ["name"] } }); // [!code focus]
}
```

<Callout type="error" title="OKM1120">
Anything else is `onConflict must be "error", "ignore", { on, update }, or { on, return: true }.` `update` and `return` together are `onConflict takes update or return, not both.`
</Callout>

<Callout type="info" title="Read committed">
The lookup is a second statement and does not set isolation. A second request that commits while the first is waiting is visible, so both callers get the row. Repeatable read and serializable can still miss it. On an archivable table, a unique that is not the primary key matches only a row that is not archived.
</Callout>

### List

`update` of a list writes those rows in one statement. A long list is split so the parameters stay inside the budget. A finished statement over 65,535 parameters is OKM1126 (count and limit in the message). The limit is for the whole statement — filters, `inList`, `limit`, and values — not for `inList` alone. Chunk the work, or filter with `= any(array)`.

| Input | Result |
| --- | --- |
| the same target twice | OKM1120. |
| a target that matches nothing | Not an error. `count` is the rows changed. |
| an empty `set` | OKM1120. |
| an empty list | No statement. |

```ts title="list.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", { id: t.identity(), title: t.text() });
const app = schema({ tables: [notes] });

export async function rename(url: string, first: string, second: string) {
  await using db = connect(url, { schema: app });
  return db.notes.update([
    { where: { id: first }, set: { title: "A" } }, // [!code focus]
    { where: { id: second }, set: { title: "B" } },
  ]);
}
```

<Callout type="error" title="OKM1120">
The same target twice is `update on notes lists the same target twice.` An empty `set` is `update set is empty.`
</Callout>

### Increment

`inc` adds to a number or bigint column inside `set`. A null number stays null. `undefined` in `set` leaves the field unchanged.

```ts title="inc.ts"
import { connect } from "okmodel/pg/postgresjs";
import { inc, schema, table, t } from "okmodel/pg";

const tasks = table("tasks", { id: t.identity(), amount: t.integer() });
const app = schema({ tables: [tasks] });

export async function add(url: string, id: string) {
  await using db = connect(url, { schema: app });
  return db.tasks.update({ where: { id }, set: { amount: inc(1) } }); // [!code focus]
}
```

<Callout type="error" title="OKM1124">
A text column is `inc adds to a number. title is a text column.`
</Callout>

### Claim

`update` with `lock: "skip"` and `limit` changes up to that many matching rows and leaves rows another transaction already holds. The database chooses which unlocked rows. The result is `{ count }`, the number of rows changed. `{ returning: true }` returns the updated rows and honors `select`, including when `{ allow }` names a guarded field. Set a `claimedBy` token in `set`, then `find` by that token, or pass `returning: true` and read the rows from the claim. `delete` does not accept `{ returning: true }`. That call is OKM1120 and deletes nothing. Pass the field names, or omit `returning` for `{ count }`.

| Input | Result |
| --- | --- |
| `limit` without `lock: "skip"`, or `lock: "skip"` without `limit` | Refused. |
| `orderBy` | Not accepted. |
| `wait` on an update | Refused. |
| `lock: "skip"` on a read | Refused. A read uses `lock: "update"` or `"share"` and `wait: "nowait"` or `"skip"` inside `tx()`. |
| a path-tenancy parent in the same statement | Refused. |

```ts title="claim.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const jobs = table("jobs", {
  id: t.identity(),
  status: t.text(),
  claimedBy: t.text().nullable(),
});
const app = schema({ tables: [jobs] });

export async function take(url: string, token: string) {
  await using db = connect(url, { schema: app });
  const claimed = await db.jobs.update({
    where: { status: "pending" },
    set: { status: "running", claimedBy: token }, // [!code focus]
    limit: 1,
    lock: "skip",
  });
  if (claimed.count === 0) return [];
  return db.jobs.find({ where: { claimedBy: token }, limit: claimed.count }); // [!code focus]
}
```

<Callout type="error" title="OKM1120">
`limit` without the lock is `limit on an update needs lock: "skip".` The lock without `limit` is `lock: "skip" needs a limit.` `wait` is `wait belongs on a read. An update claims rows with lock: "skip".` A path column in the same claim is `lock: "skip" on tasks cannot change projectId in the same claim.`
</Callout>

<Callout type="error" title="OKM1121">
`limit` must be a whole number from 1 to 2147483647.
</Callout>

### Period

`bucket(column, unit, { tz, as })` goes in `aggregate` `groupBy`, beside field names. `as` is the result key. A grouped aggregate needs `limit`, or `.all(reason)`. There is no `having` and no gap filling.

| Unit | On `timestamptz` | On `date` |
| --- | --- | --- |
| `second`, `minute`, `hour` | `Temporal.Instant`, the period start. | OKM1120. |
| `day`, `week`, `month`, `quarter`, `year` | `Temporal.PlainDate`, the local date the period starts. | The same date. |

`tz` is required for `timestamptz` and refused for `date`. A `timestamp` without a zone is refused. `weekStart` applies only to `week` and is `monday` (the default), `sunday`, or `saturday`. A NULL timestamp is one group: the key is present and the value is null.

#### simple

One day bucket on `timestamptz`. `tz` is required. A `date` column omits `tz`.

```ts title="buckets.ts"
import { connect } from "okmodel/pg/postgresjs";
import { bucket, schema, table, t } from "okmodel/pg";

const events = table("events", { id: t.identity(), at: t.timestamptz() });
const app = schema({ tables: [events] });

export async function perDay(url: string) {
  await using db = connect(url, { schema: app });
  return db.events.aggregate({
    groupBy: [bucket("at", "day", { tz: "UTC", as: "day" })], // [!code focus]
    count: true,
    limit: 31,
  });
}
```

#### complex

A week starts on Monday unless `weekStart` says otherwise. `sum` is the total `amount` in that week.

```ts title="weeks.ts"
import { connect } from "okmodel/pg/postgresjs";
import { bucket, schema, table, t } from "okmodel/pg";

const events = table("events", {
  id: t.identity(),
  at: t.timestamptz(),
  amount: t.integer(),
});
const app = schema({ tables: [events] });

export async function perWeek(url: string) {
  await using db = connect(url, { schema: app });
  return db.events.aggregate({
    groupBy: [bucket("at", "week", { tz: "UTC", weekStart: "sunday", as: "week" })], // [!code focus]
    count: true,
    sum: ["amount"],
    limit: 12,
  });
}
```

<Callout type="error" title="OKM1120">
An empty `as` is refused. `__proto__`, `constructor`, `toString`, and `hasOwnProperty` are refused. An `as` that clashes with a group field, another bucket, or `count`, `sum`, `avg`, `min`, or `max` is refused.
</Callout>

### Raw SQL

`raw` and `explain` are functions. Pass the table handle, including one from `for()`. The class names the row: each field needs an initializer, and that order is the select list. `"read"` is one select. `"write"` runs on the primary. A scoped tenant read keeps the tenant predicate outside the fragment, so the select list names the primary key with `ident`.

There is no untyped `sql.raw(text, reason)` in this version. A key that is a field of the handle's table uses that field's decode. Any other key (a joined column, an expression) uses the initializer: a number, a boolean, or driver text. Joined columns are not projected through another table's codecs.

```ts title="raw-notes.ts"
import { connect } from "okmodel/pg/postgresjs";
import { fragment, ident, raw, schema, table, t } from "okmodel/pg";

class NoteRow {
  id = "";
  title = "";
}

const notes = table("notes", { id: t.id(), title: t.text() });
const app = schema({ tables: [notes] });

export async function titled(url: string) {
  await using db = connect(url, { schema: app });
  return raw(
    db.notes,
    NoteRow,
    "read",
    fragment`select ${ident("id")}, ${ident("title")} from notes`, // [!code focus]
  );
}
```

<Callout type="error" title="OKM1602">
Pass `"read"` or `"write"`, a `fragment` template, and a class with an initializer on every column. A read starts with `select`. A write starts with `insert`, `update`, or `delete`.
</Callout>

<Callout type="error" title="OKM1702">
A raw write on a scoped tenant table sends nothing. A raw read must select the primary key with `ident` and must not alias it. `unscoped("reason")` is the hatch for a reviewed statement.
</Callout>

### Explain

`explain` describes a query you already built. Outside production it returns the `EXPLAIN` lines with the statement text and parameters. In production it does not send the statement: `plan` is null and `params` is empty.

```ts title="explain-notes.ts"
import { connect } from "okmodel/pg/postgresjs";
import { explain, schema, table, t } from "okmodel/pg";

const notes = table("notes", { id: t.id(), title: t.text() });
const app = schema({ tables: [notes] });

export async function plan(url: string) {
  await using db = connect(url, { schema: app });
  return explain(db.notes, db.notes.find({ limit: 1 })); // [!code focus]
}
```


# Required rows (/docs/define/table/reference-rows)

<Callout type="info" title="Not a foreign key">
[References](/docs/define/table/references) is `.references()`. This page is the table option `reference`: rows the application expects to find already stored.
</Callout>

A status, a role, or a plan is a row other code assumes is there. You list those rows next to the table. `okm migrate apply` inserts one when its key is not stored yet. A row that is already stored is left as it is. A row you take off the list is not deleted.

A migration file changes the catalog. An `insert` in that file is OKM1542: it changes rows outside `backfill()`. These rows are not a migration step, so that check does not see them. They are not copied into the catalog, and they do not change the catalog hash. Adding a key needs no new DDL file. The next apply inserts it.

An empty database gets them when apply installs the head snapshot. A database that already has history gets them after the pending migrations. `provision()` does the same inserts on an empty target and records `provisioned@` plus the migration id.

### Key

`key` is the field that identifies a row, or several fields. The order is the order of the primary key, the unique constraint, or a unique index that has no predicate. `key` uses the field name. Snake casing and `sqlName` are what the insert and the constraint use.

An empty key is `invalid`: `Table statuses reference key is empty.` A key that does not match is `invalid`: `Reference key label on statuses is not a primary key or a unique constraint.` A table that is not in the head snapshot yet is `invalid`: `Table statuses declares reference rows and is not in the head snapshot.` Generate first, so the snapshot includes the table.

#### one

`code` is the primary key. `open` and `done` are the rows the application looks up.

```ts title="one.ts"
import { table, t } from "okmodel/pg";

export const statuses = table(
  "statuses",
  {
    code: t.text().primaryKey(),
    label: t.text(),
  },
  {
    reference: {
      key: "code", // [!code focus]
      rows: [
        { code: "open", label: "Open" },
        { code: "done", label: "Done" },
      ],
    },
  },
);
```

#### several

The key is `kind` then `code`, the same order as `primaryKey`. `["code", "kind"]` does not match that constraint.

```ts title="several.ts"
import { table, t } from "okmodel/pg";

export const codes = table(
  "codes",
  {
    kind: t.text(),
    code: t.text(),
    label: t.text(),
  },
  {
    primaryKey: ["kind", "code"],
    reference: {
      key: ["kind", "code"], // [!code focus]
      rows: [
        { kind: "status", code: "open", label: "Open" },
        { kind: "status", code: "done", label: "Done" },
      ],
    },
  },
);
```

### Rows

`rows` is a list. Each item is an object of field names. A value is a string, a number, a boolean, or null. The key fields are required on every row. Other fields are inserted with them.

A value that is not a list is `invalid`: `Table statuses reference rows must be a list.` A row that is not an object is `invalid`: `Table statuses reference row 0 is not an object.` A missing key is `invalid`: `Table statuses reference row 0 is missing key code.` An unknown field is `invalid`: `Table statuses reference names rank, which is not a column.` Anything else is `invalid`: `Table statuses reference row 0 column label is not a string, number, boolean, or null.`

```ts title="values.ts"
import { table, t } from "okmodel/pg";

export const plans = table(
  "plans",
  {
    code: t.text().primaryKey(),
    rank: t.integer(),
    active: t.boolean(),
    note: t.text().nullable(),
  },
  {
    reference: {
      key: "code",
      rows: [
        { code: "free", rank: 1, active: true, note: null }, // [!code focus]
        { code: "pro", rank: 2, active: false, note: "paid" },
      ],
    },
  },
);
```

### Apply

Each row is its own insert. A missing key is inserted. A stored key is skipped, including when the label in this list has changed. Nothing is updated or deleted.

```sql
insert into "public"."statuses" ("code", "label") values ('open', 'Open') on conflict ("code") do nothing
```

#### simple

Two statuses. Apply inserts `open` and `done` when those codes are absent.

```ts title="simple.ts"
import { table, t } from "okmodel/pg";

export const statuses = table(
  "statuses",
  {
    code: t.text().primaryKey(),
    label: t.text(),
  },
  {
    reference: {
      key: "code",
      rows: [
        { code: "open", label: "Open" }, // [!code focus]
        { code: "done", label: "Done" },
      ],
    },
  },
);
```

#### complex

The key is two columns. The row also stores a number, a boolean, and null. A later apply that adds `{ kind: "status", code: "archived", ... }` inserts that key and leaves `open` and `done` as they are.

```ts title="complex.ts"
import { table, t } from "okmodel/pg";

export const codes = table(
  "codes",
  {
    kind: t.text(),
    code: t.text(),
    label: t.text(),
    rank: t.integer(),
    active: t.boolean(),
    note: t.text().nullable(),
  },
  {
    primaryKey: ["kind", "code"],
    reference: {
      key: ["kind", "code"], // [!code focus]
      rows: [
        { kind: "status", code: "open", label: "Open", rank: 1, active: true, note: null },
        { kind: "status", code: "done", label: "Done", rank: 2, active: false, note: null },
      ],
    },
  },
);
```


# References (/docs/define/table/references)

<Callout type="info" title="Where this fits">
The column stores another row's key. This page is that foreign key: the target, then what happens on delete and on update. [Relations](/docs/define/table/relations) are the name you query.
</Callout>

`.references(table, options)` stores the foreign key on the column. The table argument is a name, so the column type does not cycle. A later `.references()` replaces the earlier one. `schema()` checks the target. You do not name the constraint. It is `{table}_{column}_fkey`, using the SQL column name.

A new table adds it with `alter table … add constraint`. A table that already exists adds it `not valid`, then `validate constraint`. The primary key is created with the table. A foreign key aimed at a unique waits until that unique exists.

### Target

The first argument is the target table name. An empty name is OKM1060: `references() needs a table name.` The local column type has to match the target column. `t.identity()` is `bigint`, so the key that points at it is `t.bigint()`.

```ts title="target.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes"), // [!code focus]
  body: t.text(),
});

export const app = schema({ tables: [notes, comments] });
```

The name can be this table.

```ts title="parent.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  parentId: t.bigint().nullable().references("notes", { onDelete: "set null" }), // [!code focus]
});
```

<Callout type="error" title="OKM1020">
`schema()` refuses a target that is not in the schema. The message names the table and lists the accepted names.
</Callout>

### On delete

`onDelete` runs when the referenced row is deleted. Omitted, the SQL has no `on delete` clause. The value is one of the five actions below. Anything else is OKM1060, and the message lists the accepted actions.

#### omitted

Leave `onDelete` off. The clause is not written. The [target](#target) sample does that.

```ts title="delete-omitted.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes"), // [!code focus]
  body: t.text(),
});
```

#### cascade

`"cascade"` deletes the rows that point at the deleted row.

```ts title="cascade.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes", { onDelete: "cascade" }), // [!code focus]
  body: t.text(),
});
```

#### no action

`"no action"` writes `on delete no action`. The delete is refused when a row still points at the target. The check is at the end of the statement.

```ts title="no-action.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes", { onDelete: "no action" }), // [!code focus]
  body: t.text(),
});
```

#### restrict

`"restrict"` writes `on delete restrict`. The delete is refused when a row still points at the target. The check runs immediately.

```ts title="restrict.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes", { onDelete: "restrict" }), // [!code focus]
  body: t.text(),
});
```

#### set default

`"set default"` sets this column to its default. A default is not required for the definition to compile.

```ts title="set-default.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().default("0").references("notes", { onDelete: "set default" }), // [!code focus]
  body: t.text(),
});
```

#### set null

`"set null"` sets this column to null. The column has to be nullable. A not-null column is OKM1060: `Foreign key comments.noteId uses set null. comments.noteId must be nullable.`

```ts title="set-null.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().nullable().references("notes", { onDelete: "set null" }), // [!code focus]
  body: t.text(),
});
```

<Callout type="error" title="OKM1060">
On a tenant table, the foreign key includes the tenant key. `set null` and `set default` are then refused, on delete or on update. The message names the key and lists `cascade`, `restrict`, and `no action`. See [tenancy](/docs/define/table/tenancy).
</Callout>

### On update

`onUpdate` runs when the referenced key changes. It takes the same five actions. Both options can be set on one call. Anything else is OKM1060, and the message lists the accepted actions.

#### omitted

Leave `onUpdate` off. The SQL has no `on update` clause. The [target](#target) sample does that.

```ts title="update-omitted.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes"), // [!code focus]
  body: t.text(),
});
```

#### cascade

`"cascade"` copies the new key into this column. This sample sets both actions.

```ts title="update-cascade.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes", { onDelete: "cascade", onUpdate: "cascade" }), // [!code focus]
  body: t.text(),
});
```

#### no action

`"no action"` writes `on update no action`. The key change is refused when a row still points at the old key. The check is at the end of the statement.

```ts title="update-no-action.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes", { onUpdate: "no action" }), // [!code focus]
  body: t.text(),
});
```

#### restrict

`"restrict"` writes `on update restrict`. The key change is refused when a row still points at the old key. The check runs immediately.

```ts title="update-restrict.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes", { onUpdate: "restrict" }), // [!code focus]
  body: t.text(),
});
```

#### set default

`"set default"` sets this column to its default. A default is not required for the definition to compile.

```ts title="update-set-default.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().default("0").references("notes", { onUpdate: "set default" }), // [!code focus]
  body: t.text(),
});
```

#### set null

`"set null"` sets this column to null. The column has to be nullable, same as on delete.

```ts title="update-set-null.ts"
import { table, t } from "okmodel/pg";

export const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().nullable().references("notes", { onUpdate: "set null" }), // [!code focus]
  body: t.text(),
});
```

### Columns

`columns` names the target column. Omitted, `schema()` uses that table's primary key when the key is one column.

#### omitted

No `columns` means the target's primary key. No primary key is OKM1021. A primary key of several columns is OKM1021, and the message lists those columns.

```ts title="primary.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes"), // [!code focus]
  body: t.text(),
});

export const app = schema({ tables: [notes, comments] });
```

#### one name

The list holds one field name, or that column's SQL name. That column is the primary key or a unique. `schema()` checks that the name exists and the types match. It does not check uniqueness. Postgres refuses the constraint when the target is neither. An empty list is OKM1060: `references() columns must name at least one target column.` An empty name is OKM1060: `references() columns must be non-empty names.`

```ts title="target-columns.ts"
import { schema, table, t } from "okmodel/pg";

const users = table("users", {
  id: t.uuid().primaryKey(),
  email: t.text(),
});

const comments = table("comments", {
  id: t.identity(),
  authorId: t.uuid().references("users", { columns: ["id"] }), // [!code focus]
  body: t.text(),
});

export const app = schema({ tables: [users, comments] });
```

<Callout type="error" title="OKM1021">
A missing column is refused, and so is more than one name. The message lists the columns on the target.
</Callout>

<Callout type="error" title="OKM1022">
The local type and the target type must match. The message names both types and the type that is accepted.
</Callout>

<Callout type="info" title="Both">
A [relation](/docs/define/table/relations) is the name you query. The reference is the foreign key. You want both: the column stores the key, and `one` or `many` names the edge.
</Callout>


# Relations (/docs/define/table/relations)

<Callout type="info" title="Where this fits">
The foreign key is the column. This page is the edge you include: one row, many rows, a join table, or one relation across several tables.
</Callout>

The key in `relations` is the name you pass to `include` and to `where`. `schema()` finds the foreign key. A value that is not `one()`, `many()`, `manyThrough()`, `morph()`, or `morphMany()` is OKM1061.

<Callout type="error" title="OKM1020">
A missing table is refused. The message names it and lists the accepted names. For a join, that includes the join table.
</Callout>

### To-one

`one(table, field)` is one related row. Include returns that row, or null when it is missing. `true` loads the row. `select`, `where`, and `orderBy` narrow it.

#### table

The argument is the related table name. The foreign key is a column on this table that points there.

```ts title="to-one.ts"
import { connect } from "okmodel/pg/postgresjs";
import { one, schema, table, t } from "okmodel/pg";

const users = table("users", {
  id: t.identity(),
  name: t.text(),
});
const notes = table(
  "notes",
  {
    id: t.identity(),
    authorId: t.bigint().references("users"),
    title: t.text(),
  },
  { relations: { author: one("users") } }, // [!code focus]
);
const app = schema({ tables: [users, notes] });

export async function authorName(url: string) {
  await using db = connect(url, { schema: app });
  const rows = await db.notes.find({ limit: 20, include: { author: true } }); // [!code focus]
  return rows[0]?.author?.name;
}
```

#### field

Pass `field` when this table has more than one foreign key to that table. It is the local field name. Omit it when there is one key. Zero keys and more than one key are both OKM1021. The message lists the columns, or `none`.

```ts title="two-keys.ts"
import { one, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    authorId: t.bigint().references("users"),
    editorId: t.bigint().references("users"),
    title: t.text(),
  },
  {
    relations: {
      author: one("users", "authorId"), // [!code focus]
      editor: one("users", "editorId"), // [!code focus]
    },
  },
);
```

<Callout type="error" title="OKM1021">
`Relation notes.author on users is ambiguous. Accepted columns: authorId, editorId.`
</Callout>

### To-many

`many(table, field)` is the rows that point here. Include returns a list. A to-many include needs `limit`, or the call is OKM1105: `Include comments is to-many and has no limit. Pass limit, or call .all(reason).`

#### table

The argument is the other table. The foreign key is a column on that table that points here.

```ts title="to-many.ts"
import { connect } from "okmodel/pg/postgresjs";
import { many, schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { relations: { comments: many("comments") } }, // [!code focus]
);
const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes"),
  body: t.text(),
});
const app = schema({ tables: [notes, comments] });

export async function firstComment(url: string) {
  await using db = connect(url, { schema: app });
  const rows = await db.notes.find({
    limit: 20,
    include: { comments: { limit: 1 } }, // [!code focus]
  });
  return rows[0]?.comments[0]?.body;
}
```

#### field

Pass `field` when the other table has more than one foreign key here. It is that table's field name. Omit it when there is one key. Zero keys and more than one key are both OKM1021.

```ts title="many-field.ts"
import { many, table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { relations: { edits: many("comments", "noteId") } }, // [!code focus]
);
```

### Filter

`has`, `none`, and `every` go in `where` under the relation name. A morph to-one refuses them: OKM1120, and the message says to filter the type and id columns or include the relation.

| Call | Keeps a row when |
| --- | --- |
| `has` | At least one related row matches. |
| `none` | No related row matches. |
| `every` | Every related row matches. No related rows also matches. |

#### has

`has` keeps a row that has a match.

```ts title="has.ts"
import { connect } from "okmodel/pg/postgresjs";
import { has, many, schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { relations: { comments: many("comments") } },
);
const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes"),
  body: t.text(),
});
const app = schema({ tables: [notes, comments] });

export async function discussed(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.find({
    limit: 20,
    where: { comments: has({ body: "ship it" }) }, // [!code focus]
  });
}
```

#### none

`none` keeps a row that has no match.

```ts title="none.ts"
import { connect } from "okmodel/pg/postgresjs";
import { many, none, schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { relations: { comments: many("comments") } },
);
const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes"),
  body: t.text(),
});
const app = schema({ tables: [notes, comments] });

export async function quiet(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.find({
    limit: 20,
    where: { comments: none({ body: "ship it" }) }, // [!code focus]
  });
}
```

#### every

`every` keeps a row whose related rows all match.

```ts title="every.ts"
import { connect } from "okmodel/pg/postgresjs";
import { every, many, schema, table, t } from "okmodel/pg";

const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { relations: { comments: many("comments") } },
);
const comments = table("comments", {
  id: t.identity(),
  noteId: t.bigint().references("notes"),
  body: t.text(),
});
const app = schema({ tables: [notes, comments] });

export async function agreed(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.find({
    limit: 20,
    where: { comments: every({ body: "ship it" }) }, // [!code focus]
  });
}
```

### Join table

`manyThrough(table, { through, from, to })` is to-many through a join table. The join table has one foreign key to this table and one to the related table. Include is to-many, so it needs `limit`. Tenancy and archive apply to the join rows and to the related rows.

#### through

`through` is the join table name.

```ts title="through.ts"
import { manyThrough, schema, table, t } from "okmodel/pg";

const teams = table("teams", { id: t.identity(), name: t.text() });
const memberships = table("memberships", {
  id: t.identity(),
  userId: t.bigint().references("users"),
  teamId: t.bigint().references("teams"),
});
const users = table(
  "users",
  { id: t.identity(), name: t.text() },
  { relations: { teams: manyThrough("teams", { through: "memberships" }) } }, // [!code focus]
);

export const app = schema({ tables: [users, teams, memberships] });
```

#### from

`from` is the join-table field that references this table. Pass it when more than one foreign key on the join table points here.

```ts title="from.ts"
import { manyThrough, table, t } from "okmodel/pg";

export const users = table(
  "users",
  { id: t.identity(), name: t.text() },
  {
    relations: {
      teams: manyThrough("teams", { through: "memberships", from: "userId" }), // [!code focus]
    },
  },
);
```

#### to

`to` is the join-table field that references the related table. Pass it when more than one foreign key points there. The same field for both ends is OKM1021.

```ts title="from-to.ts"
import { manyThrough, table, t } from "okmodel/pg";

export const users = table(
  "users",
  { id: t.identity(), name: t.text() },
  {
    relations: {
      teams: manyThrough("teams", { through: "memberships", from: "userId", to: "teamId" }), // [!code focus]
    },
  },
);
```

<Callout type="error" title="OKM1021">
`Relation users.teams through memberships is ambiguous at from. Accepted columns: (none).` The same field for both ends says `Pass from and to.`
</Callout>

### Morph

One table points at a closed list of others. Include returns the matching row, with the type field set to that table's name, or null when the row is missing, archived, outside the tenant, or the stored type is outside the list.

#### morph

`morph(name, targets)` is declared on the table that stores the pointer. `name` is the column stem. The targets are table names. An empty name, an empty target, or a target missing from the schema is OKM1020. The same target twice is OKM1023.

The table has `${name}Type` and `${name}Id`. The type column is text, `varchar`, `char`, or `citext`. A `varchar(n)` has to fit the longest target name. The id column matches each target's primary key, and that key is one column. The two columns are both nullable, or neither. A missing column is OKM1021. A type or id mismatch is OKM1022.

```ts title="morph-declare.ts"
import { morph, table, t } from "okmodel/pg";

export const comments = table(
  "comments",
  {
    id: t.text().primaryKey(),
    body: t.text(),
    commentableType: t.text(),
    commentableId: t.text(),
  },
  { relations: { commentable: morph("commentable", ["tasks", "lists"]) } }, // [!code focus]
);
```

#### morphMany

`morphMany(table, name)` is the reverse, declared on each target. `table` holds the type and id columns. `name` is the same stem. That table must declare `morph(name, targets)` and list this table. Include is to-many, so it takes `limit`, `orderBy`, `where`, and `select`. `has`, `none`, and `every` work on this side.

```ts title="morph-many.ts"
import { morphMany, table, t } from "okmodel/pg";

export const tasks = table(
  "tasks",
  { id: t.text().primaryKey(), title: t.text() },
  { relations: { comments: morphMany("comments", "commentable") } }, // [!code focus]
);
```

#### morphTo

`morphTo(name, type, id)` sets the type and id columns together. Spread it into insert or update. Setting one without the other, or clearing one and setting the other, is OKM1121. Clearing both is allowed. A type outside the list is OKM1120, and the message lists the accepted tables. An empty name or type is OKM1020. A null name, a null type, or an id that is not a non-empty string is OKM1120. A write can store another tenant's id. The include reads as the caller, so that target is null.

```ts title="morph-to.ts"
import { morphTo } from "okmodel/pg";

export const link = {
  id: "c1",
  body: "ship it",
  ...morphTo("commentable", "tasks", "t1"), // [!code focus]
};
```

#### include

A morph include is `true`. `select`, `where`, `orderBy`, and `limit` are OKM1120. Nesting it inside another include is OKM1120. A write can store another tenant's id. The include reads as the caller, so that target is null.

```ts title="morph.ts"
import { connect } from "okmodel/pg/postgresjs";
import { morph, morphMany, morphTo, schema, table, t } from "okmodel/pg";

const tasks = table(
  "tasks",
  { id: t.text().primaryKey(), title: t.text() },
  { relations: { comments: morphMany("comments", "commentable") } },
);
const lists = table(
  "lists",
  { id: t.text().primaryKey(), name: t.text() },
  { relations: { comments: morphMany("comments", "commentable") } },
);
const comments = table(
  "comments",
  {
    id: t.text().primaryKey(),
    body: t.text(),
    commentableType: t.text(),
    commentableId: t.text(),
  },
  { relations: { commentable: morph("commentable", ["tasks", "lists"]) } },
);
const app = schema({ tables: [tasks, lists, comments] });

export async function attach(url: string, taskId: string) {
  await using db = connect(url, { schema: app });
  await db.comments.insert({
    id: "c1",
    body: "ship it",
    ...morphTo("commentable", "tasks", taskId),
  });
  const rows = await db.comments.find({ limit: 20, include: { commentable: true } }); // [!code focus]
  return rows[0]?.commentable;
}
```


# Shape (/docs/define/table/shape)

<Callout type="info" title="Where this fits">
A column is one value until you wrap it. An array repeats a scalar, a picklist narrows text, and an enum is a Postgres type. [Names](/docs/define/table/names) are what the database stores.
</Callout>

An array repeats one scalar. A picklist keeps the column as text and checks the value. An enum is its own Postgres type.

### Array

`.array()` wraps a scalar. The SQL type gains one `[]` per rank. There is no second `.array()` call to add a dimension.

An empty array is a value. An element cannot be null. `.nullable()` makes the whole column null, not an element. A picklist or an enum is wrapped after it is declared: `.picklist(values).array()` or `t.enum(name, labels).array()`.

| `dims` | Result |
| --- | --- |
| omitted | Rank 1. `text[]`. |
| `2` to `6` | That many `[]`. `2` is `integer[][]`. |
| outside 1 to 6 | Refused. |

#### one

Omit `dims` for a list.

```ts title="array.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  tags: t.text().array(), // [!code focus]
});
const app = schema({ tables: [notes] });

export async function add(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert({ tags: ["ship"] });
}
```

#### rank

`dims` is the rank. `2` is a matrix.

```ts title="matrix.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  grid: t.integer().array({ dims: 2 }), // [!code focus]
});
const app = schema({ tables: [notes] });

export async function add(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert({ grid: [[1, 2]] });
}
```

<Callout type="error" title="OKM1060">
A rank outside 1 to 6 is refused. Calling `.array()` on a column that is already an array is refused.
</Callout>

<Callout type="error" title="OKM1210">
A value that is not an array is rejected, and so is a null element. A rank that does not match `dims` is rejected when the value is read back.
</Callout>

### Picklist

`.picklist(values)` narrows a string column to that union. A default has to be one of the values. `.picklist(values).default("open")` and `.default("open").picklist(values)` both check it.

| `check` | Result |
| --- | --- |
| omitted | The catalog CHECK is `column IN (...)`, in the order you listed. |
| `false` | The list stays in the type. The catalog has no CHECK. Encode still rejects a value outside the list. |

#### check

The default writes the CHECK.

```ts title="picklist.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  status: t.text().picklist(["open", "done"]).default("open"), // [!code focus]
});
const app = schema({ tables: [notes] });

export async function close(url: string, id: string) {
  await using db = connect(url, { schema: app });
  return db.notes.update({ where: { id }, set: { status: "done" } });
}
```

#### type

`{ check: false }` keeps the union and skips the CHECK.

```ts title="picklist-type.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  status: t.text().picklist(["open", "done"], { check: false }), // [!code focus]
});
```

<Callout type="error" title="OKM1060">
An empty list, an empty value, or a repeated value is refused. A default outside the list is refused. The message names the value.
</Callout>

<Callout type="error" title="OKM1210">
A value outside the list is rejected when it is encoded. The message names the accepted values.
</Callout>

### Enum

`t.enum(name, labels)` creates a Postgres enum. The column type is that name, not `text`. Labels are stored in that order. The TypeScript type is the union of the labels. Two columns may share the name when the label lists match.

A [picklist](#picklist) stays a string column and adds a CHECK. An enum is its own type. Adding a label, with the labels you already have still in that order, plans `ALTER TYPE … ADD VALUE`, and that step runs on its own. Removing a label, or changing their order, replaces the type and rewrites each column that uses it.

```ts title="enum.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  status: t.enum("note_status", ["open", "done"]), // [!code focus]
});
const app = schema({ tables: [notes] });

export async function open(url: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert({ status: "open" });
}
```

<Callout type="error" title="OKM1060">
An empty name, an empty list, an empty label, or a repeated label is refused.
</Callout>

<Callout type="error" title="OKM1210">
A value outside the labels is rejected when it is encoded. The message names the accepted labels.
</Callout>

<Callout type="error" title="OKM1020">
The same enum name with a different label list is refused. The message names both tables.
</Callout>


# Tenancy (/docs/define/table/tenancy)

<Callout type="info" title="Where this fits">
[Schema tenancy](/docs/define/schema/tenancy) adds one key to every table. This page keeps that key, marks the table global, or follows a path of relations.
</Callout>

Omitted, the table stores the key. `global` and `via` come from `okmodel/tenancy`. Anything else is OKM1060: `Table notes tenancy must be global("reason") or via().`

### Key

The key is a guarded uuid. Insert fills it from `for()`. A read or a write filters on it. Input cannot set it. Insert that names it is OKM1190: `Field notes.tenantId is the tenant key. The scope sets it. Input cannot.` An update is OKM1704: `Field notes.tenantId is the tenant key. An update cannot change it.` A filter is the same code: `Field notes.tenantId is the tenant key. A filter cannot set it.`

A column the table already declares with that name is OKM1012: `Table notes already declares tenantId, which tenancy adds. Rename the field or drop it from the table.`

A unique includes the key. The primary key does too. An index must start with the key. One that does not is OKM1706: `Index on notes starts with title. Put tenantId first.` A foreign key to another tenant table includes the key, so `set null` and `set default` are refused. See [references](/docs/define/table/references).

#### unique

`.unique({ global: "reason" })` leaves the key out of that one constraint. `{ global: true, reason }` is the same opt-out. An empty reason is OKM1060: `unique() global needs a reason.` `{ global: true }` with no reason is OKM1060: `Unique notes.email sets global and needs a reason.`

```ts title="unique.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  email: t.text().unique({ global: "login email" }), // [!code focus]
  title: t.text().unique(),
});
```

`email` is unique across tenants. `title` is unique inside one tenant.

### Global

`global("reason")` leaves the tenant column off this table. The reason is stored and `inspect()` shows it. An empty reason is OKM1060: `global() needs a reason.`

```ts title="global.ts"
import { table, t } from "okmodel/pg";
import { global } from "okmodel/tenancy";

export const plans = table(
  "plans",
  { id: t.identity(), code: t.text() },
  { tenancy: global("plans are shared") }, // [!code focus]
);
```

<Callout type="error" title="OKM1705">
A global table that references a tenant table is `Table plans.code is global and references tenant table notes. Drop the reference, or make plans a tenant table.`
</Callout>

### Path

`via("project.organization")` follows to-one relations. Each step is a relation name, separated by `.`, up to three. The last table stores the tenant key. This table does not. A read or a write adds `exists` along that path. An empty path is OKM1060: `via() needs a relation path.`

```ts title="path.ts"
import { one, table, t } from "okmodel/pg";
import { via } from "okmodel/tenancy";

export const organizations = table("organizations", {
  id: t.identity(),
  name: t.text(),
});

export const projects = table(
  "projects",
  {
    id: t.identity(),
    organizationId: t.bigint().references("organizations"),
    name: t.text(),
  },
  { relations: { organization: one("organizations") } },
);

export const tasks = table(
  "tasks",
  {
    id: t.identity(),
    projectId: t.bigint().references("projects"),
    title: t.text(),
  },
  {
    relations: { project: one("projects") },
    tenancy: via("project.organization"), // [!code focus]
  },
);
```

<Callout type="error" title="OKM1705">
More than three steps is `Table tasks tenancy via "a.b.c.d" is longer than 3 hops.` A missing to-one relation is `Table tasks tenancy via "project.organization" has no to-one relation project.` A path that ends on a global table is `Table tasks tenancy via "project.organization" ends at plans, which is not a tenant table.` A cycle is `Table tasks tenancy via "project.organization" cycles at projects.` An insert whose parent is outside this tenant is `Insert on tasks did not find a parent in this tenant.` An update is `Update on tasks did not find a parent in this tenant.`
</Callout>

### Calls

`for({ tenantId })` opens a client for one tenant. The value is a uuid. `unscoped("reason")` drops the predicate. Insert still needs `for()`. A call with no scope is OKM1701: `Table notes is tenant-scoped. Call for({ tenantId }) or unscoped("reason").`

#### simple

One tenant table. Insert sets `title`. The scope sets `tenantId`.

```ts title="simple.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { columnTenancy } from "okmodel/tenancy";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

const app = schema({
  tables: [notes],
  tenancy: columnTenancy({ key: "tenantId", type: "uuid" }), // [!code focus]
});

export async function add(url: string, title: string) {
  await using db = connect(url, { schema: app });
  const scoped = db.for({ tenantId: "018f3b2c-7a11-7c3a-8e2d-6b4a9c1d0e5f" }); // [!code focus]
  return scoped.notes.insert({ title }); // [!code focus]
}
```

#### complex

`organizations` stores the key. `projects` follows `organization`. `tasks` follows `project.organization`. `plans` is global, so the root client can insert it. The scoped client inserts the rest.

```ts title="complex.ts"
import { connect } from "okmodel/pg/postgresjs";
import { one, schema, table, t } from "okmodel/pg";
import { columnTenancy, global, via } from "okmodel/tenancy";

const organizations = table("organizations", {
  id: t.identity(),
  name: t.text(),
});

const projects = table(
  "projects",
  {
    id: t.identity(),
    organizationId: t.bigint().references("organizations"),
    name: t.text(),
  },
  {
    relations: { organization: one("organizations") },
    tenancy: via("organization"), // [!code focus]
  },
);

const tasks = table(
  "tasks",
  {
    id: t.identity(),
    projectId: t.bigint().references("projects"),
    title: t.text(),
  },
  {
    relations: { project: one("projects") },
    tenancy: via("project.organization"), // [!code focus]
  },
);

const plans = table(
  "plans",
  { id: t.identity(), code: t.text() },
  { tenancy: global("plans are shared") }, // [!code focus]
);

const app = schema({
  tables: [organizations, projects, tasks, plans],
  tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
});

export async function open(url: string) {
  await using db = connect(url, { schema: app });
  await db.plans.insert({ code: "free" }); // [!code focus]
  const scoped = db.for({ tenantId: "018f3b2c-7a11-7c3a-8e2d-6b4a9c1d0e5f" });
  const org = await scoped.organizations.insert({ name: "Acme" });
  const project = await scoped.projects.insert({
    name: "Ship",
    organizationId: org.id, // [!code focus]
  });
  return scoped.tasks.insert({ title: "Write", projectId: project.id }); // [!code focus]
}
```


# Traits (/docs/define/table/traits)

<Callout type="info" title="Where this fits">
[Schema traits](/docs/define/schema/traits) apply to every table. This page is a trait you write, then the four that ship.
</Callout>

### Create

`trait(name, definition)` builds a trait. Import it from `okmodel/traits`. The name is stored on each column it adds. `schema()` copies those columns onto the table. Schema traits apply first. This table's list applies after them.

#### fields

Required. Column builders, the same ones a table uses. An empty object is allowed. A missing object is OKM1060: `Trait labeled needs a fields object.` An empty name is OKM1060: `trait() trait needs a name.`

```ts title="fields.ts"
import { t } from "okmodel/pg";
import { trait } from "okmodel/traits";

export const labeled = trait("labeled", {
  fields: { label: t.text() }, // [!code focus]
});
```

#### touch

Field names set to `now()` on update. Each one is sealed. A name that is not a field of this trait is OKM1060: `Trait labeled touch names nope, which is not one of its fields. Accepted fields: label, labeledAt.` A value that is not a list is `Trait labeled touch must be a list of field names.`

```ts title="touch.ts"
import { t } from "okmodel/pg";
import { trait } from "okmodel/traits";

export const labeled = trait("labeled", {
  fields: {
    label: t.text(),
    labeledAt: t.timestamptz().defaultSql("now()"),
  },
  touch: ["labeledAt"], // [!code focus]
});
```

#### sealed

Field names input cannot set, including `{ allow }`. A touched field is sealed even when this list omits it. A name outside `fields` uses the same OKM1060 as `touch`.

```ts title="sealed.ts"
import { t } from "okmodel/pg";
import { trait } from "okmodel/traits";

export const labeled = trait("labeled", {
  fields: { label: t.text().default("x") },
  sealed: ["label"], // [!code focus]
});
```

#### presets

A named `where` on every table that uses the trait. The preset sees these columns, not the table's. A client method is OKM1040: `Preset find on trait labeled is a client method or a reserved name.` A name a table or another trait already uses is OKM1040: `Preset marked on notes is defined by table notes and by trait labeled.`

```ts title="presets.ts"
import { t } from "okmodel/pg";
import { trait } from "okmodel/traits";

export const labeled = trait("labeled", {
  fields: { label: t.text().default("x") },
  presets: {
    marked: (q) => q.where({ label: "x" }), // [!code focus]
  },
});
```

<Callout type="error" title="OKM1061">
`methods` and `requires` are refused: `Trait option methods is not available yet. It arrives in 0.2.`
</Callout>

### On a table

`traits` is a list. A value that is not a list is OKM1060: `Table notes traits must be a list of traits.`

#### simple

One field. Insert sets it.

```ts title="simple.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { trait } from "okmodel/traits";

const labeled = trait("labeled", { fields: { label: t.text() } }); // [!code focus]

const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { traits: [labeled] }, // [!code focus]
);
const app = schema({ tables: [notes] });

export async function add(url: string, title: string, label: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert({ title, label }); // [!code focus]
}
```

#### complex

`fields`, `touch`, `sealed`, and `presets` together. Insert omits the sealed columns. Update changes `title`, and `labeledAt` moves. `marked()` is the preset.

```ts title="complex.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { trait } from "okmodel/traits";

const labeled = trait("labeled", {
  fields: {
    label: t.text().default("x"), // [!code focus]
    labeledAt: t.timestamptz().defaultSql("now()"), // [!code focus]
  },
  touch: ["labeledAt"], // [!code focus]
  sealed: ["label"], // [!code focus]
  presets: {
    marked: (q) => q.where({ label: "x" }), // [!code focus]
  },
});

const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { traits: [labeled] },
);
const app = schema({ tables: [notes] });

export async function rename(url: string, title: string) {
  await using db = connect(url, { schema: app });
  const note = await db.notes.insert({ title }); // [!code focus]
  await db.notes.update({ where: { id: note.id }, set: { title: "Next" } });
  return db.notes.marked().find({ limit: 1 }); // [!code focus]
}
```

<Callout type="error" title="OKM1012">
A column the table already has is `Trait labeled adds notes.label, and notes already declares it.` A column another trait already added is `Trait labeled adds notes.label, and trait timestamps already adds it.`
</Callout>

### Omit defaults

`omitDefaults` is the reason. It drops the schema list and keeps this table's `traits`. An empty reason, or one that is only spaces, is OKM1060: `Table sessions omitDefaults needs a reason.`

```ts title="sessions.ts"
import { table, t } from "okmodel/pg";

export const sessions = table(
  "sessions",
  {
    id: t.identity(),
    token: t.text(),
  },
  { omitDefaults: "sessions are not stamped" }, // [!code focus]
);
```

### Built-in

`timestamps`, `archivable`, `versioned`, and `sortable` come from `okmodel/traits`.

| Trait | Columns | What you pass |
| --- | --- | --- |
| `timestamps()` | `createdAt`, `updatedAt` | `{ enforce: "trigger" }` for a database trigger. |
| `archivable()` | `archivedAt`, `archiveId` | `cascade`, `strategy`. |
| `versioned()` | `version` | Nothing. |
| `sortable(groupBy?)` | `position` | A column name, or nothing for one list. |

#### Timestamps

Both columns are `timestamptz`, not null, default `now()`, guarded and sealed. Insert leaves them to the default. Update sets `updatedAt` and leaves `createdAt`. `{ enforce: "trigger" }` also writes `updatedAt` in the database. Any other option is refused.

```ts title="timestamps.ts"
import { table, t } from "okmodel/pg";
import { timestamps } from "okmodel/traits";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  { traits: [timestamps({ enforce: "trigger" })] }, // [!code focus]
);
```

Insert omits both columns. The row comes back with them set. Update names the fields you change. `updatedAt` moves. `createdAt` does not.

```ts title="stamp.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { timestamps } from "okmodel/traits";

const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { traits: [timestamps()] },
);
const app = schema({ tables: [notes] });

export async function rename(url: string, title: string) {
  await using db = connect(url, { schema: app });
  const note = await db.notes.insert({ title }); // [!code focus]
  return db.notes.update({ where: { id: note.id }, set: { title: "Next" } });
}
```

#### Archive

`archivedAt` is `timestamptz` and nullable. `archiveId` is `uuid` and nullable. Both are guarded and sealed. Uniques on that table become partial. `cascade` names child tables archived and restored with the row. An empty name is refused. `strategy: "column"` keeps archived rows in the table.

```ts title="archive.ts"
import { table, t } from "okmodel/pg";
import { archivable } from "okmodel/traits";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  { traits: [archivable({ cascade: ["comments"] })] }, // [!code focus]
);
```

<Callout type="error" title="OKM1061">
`strategy: "table"` is not available yet.
</Callout>

`archive({ where })` sets `archivedAt` and one new `archiveId`, shared with the cascaded children. It returns `{ count, archiveId }`. A later `find` does not see that row. `withArchived()` includes it. `onlyArchived()` is the archived set only. `restore({ archiveId })` restores that whole operation. `restore({ where })` restores those rows and the children that carry their `archiveId`.

```ts title="retire.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { archivable } from "okmodel/traits";

const notes = table(
  "notes",
  { id: t.identity(), title: t.text() },
  { traits: [archivable()] },
);
const app = schema({ tables: [notes] });

export async function retire(url: string, id: string) {
  await using db = connect(url, { schema: app });
  const saved = await db.notes.archive({ where: { id } }); // [!code focus]
  const still = await db.notes.withArchived().find({ where: { id }, limit: 1 });
  await db.notes.restore({ archiveId: saved.archiveId });
  return still;
}
```

<Callout type="error" title="OKM1052">
`archive` and `restore` on a table that is not archivable are refused.
</Callout>

#### Versioned

`version` starts at 1. A stale write is a conflict and changes nothing. A miss stays `{ count: 0 }`. `inc` and the other atomic writes still bump `version`. `delete`, `archive`, and `restore` do not check it. Read the row, then pass `version` back on the update. The write bumps it.

```ts title="versioned.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { versioned } from "okmodel/traits";

const tasks = table(
  "tasks",
  { id: t.text().primaryKey(), title: t.text() },
  { traits: [versioned()] }, // [!code focus]
);
const app = schema({ tables: [tasks] });

export async function edit(url: string, id: string) {
  await using db = connect(url, { schema: app });
  const current = await db.tasks.one({ where: { id } });
  if (current === null) return null;
  return db.tasks.update({
    where: { id },
    set: { title: "next" },
    version: current.version, // [!code focus]
  });
}
```

<Callout type="error" title="OKM1120">
Leaving `version` out is refused, and so is `version` on a table that is not versioned. An upsert and `.all()` are refused.
</Callout>

#### Sortable

New rows go at the end of the group. `find` and `page` order by `position` when you omit `orderBy`. `one()` does not. A move rewrites every row in the group. A delete leaves a gap until the next `move`. `move(id, { before })` or `{ after }` places that row next to the anchor in the same group. The result is `{ position }`.

```ts title="sortable.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { sortable } from "okmodel/traits";

const tasks = table(
  "tasks",
  { id: t.text().primaryKey(), title: t.text(), laneId: t.text() },
  { traits: [sortable("laneId")] }, // [!code focus]
);
const app = schema({ tables: [tasks] });

export async function place(url: string, id: string, otherId: string) {
  await using db = connect(url, { schema: app });
  return db.tasks.move(id, { before: otherId }); // [!code focus]
}
```

<Callout type="error" title="OKM1120">
Naming the group column in `set` is refused, even when the value is the one already stored.
</Callout>

<Callout type="error" title="OKM1060">
`via()` on this table is refused. `sortable()` cannot group by `position`. An empty group name is refused.
</Callout>


# Validation (/docs/define/table/validation)

<Callout type="info" title="Where this fits">
The column has a type. A rule here rejects a value that type still allows. It runs only when validation is on and the engine has loaded. [References](/docs/define/table/references) point a column at another table.
</Callout>

A field's rules live in one place. Insert and update run them when validation is on and `okmodel/validate` is loaded. A read does not. `{ validate: false }` skips that call. A delete does not validate.

### Inline

`.validate()` stores a list of [built-in](#trim) rules on the column, or one [Standard Schema](#standard-schema).

```ts title="notes.ts"
import { table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

export const notes = table("notes", {
  id: t.identity(),
  title: t.varchar(200).validate([v.trim(), v.min(1, "title_required")]), // [!code focus]
  email: t.text().validate([v.lowercase(), v.email("email_invalid")]), // [!code focus]
});
```

### Section

`validate` is the section. Each key is a field, and `$row` is the whole row. Every value is a list of rules. A key that is not a column is OKM1060. A value that is not a list is OKM1060. A `$row` rule sees the row after the field transforms.

```ts title="rows.ts"
import { table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
    status: t.text().picklist(["draft", "active"]),
    qty: t.integer(),
  },
  {
    validate: { // [!code focus]
      title: [v.min(1, "title_required")], // [!code focus]
      $row: [ // [!code focus]
        v.rule( // [!code focus]
          (row: { readonly status?: string; readonly qty?: number }) => // [!code focus]
            row.status !== "active" || (row.qty ?? 0) > 0, // [!code focus]
          "qty", // [!code focus]
          "qty_required", // [!code focus]
        ), // [!code focus]
      ], // [!code focus]
    }, // [!code focus]
  },
);
```

<Callout type="error" title="OKM1030">
The same field in `.validate()` and in this section is refused. `okm check` reports it, and so does the first validated call.
</Callout>

### Built-in

`v` comes from `okmodel/validate`. Importing it loads the engine. `trim` and `lowercase` run before the checks. A string is changed, and that changed value is what the write stores. Any other value is left as it is. `message` on an issue is the key you pass, not display text.

<Callout type="error" title="OKM1201">
A write that validates without `okmodel/validate` imported is refused. `okm check` reports it.
</Callout>

#### trim

`v.trim()` takes no argument. It removes leading and trailing whitespace from a string. The write stores that trimmed string.

```ts title="trim.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([v.trim()]), // [!code focus]
});
const app = schema({ tables: [notes], validation: true });

export async function add(url: string) {
  await using db = connect(url, { schema: app });
  const row = await db.notes.insert.validate({ title: "  hello  " });
  return row.title;
}
```

#### lowercase

`v.lowercase()` takes no argument. It lowercases a string. A non-string is unchanged. The write stores the lowercased string.

```ts title="lower.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  email: t.text().validate([v.lowercase()]), // [!code focus]
});
const app = schema({ tables: [notes], validation: true });

export async function add(url: string) {
  await using db = connect(url, { schema: app });
  const row = await db.notes.insert.validate({ email: "A@B.C" });
  return row.email;
}
```

#### email

`v.email(key)` takes the issue key. A string must have one `@`, a dot after it, and no spaces. Any other value fails. The issue path is the field name.

```ts title="email.ts"
import { table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

export const people = table("people", {
  id: t.identity(),
  email: t.text().validate([v.email("email_invalid")]), // [!code focus]
});
```

#### min

`v.min(n, key)` takes the minimum and the issue key. A string is measured in code points and must be at least `n`. A number must be at least `n`. Any other value fails. The issue path is the field name.

```ts title="min.ts"
import { table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

export const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([v.min(1, "title_required")]), // [!code focus]
  qty: t.integer().validate([v.min(1, "qty_min")]), // [!code focus]
});
```

#### rule

`v.rule(fn, path, key)` takes the predicate, the issue path, and the issue key. `false` records `key` at `path`. That path is the segment you pass. It is not the field name plus the segment. On a field, `fn` receives the field value. On [`$row`](#section), `fn` receives the row after the field transforms.

```ts title="reserved.ts"
import { table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

export const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([
    v.rule((value: string) => value !== "admin", "title", "reserved"), // [!code focus]
  ]),
});
```

#### onInsert

`v.onInsert()` with no argument is a marker, not a check. Rules before it run on insert and update. Rules after it run only on insert, until the next `onInsert()` or `onUpdate()`. A transform after the marker changes the stored value only on insert. A Standard Schema in the list is not limited by the marker.

Pass one rule and only that rule is limited to insert. Rules after that call are not.

```ts title="insert-phase.ts"
import { table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

export const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([
    v.min(1, "title_required"),
    v.onInsert(), // [!code focus]
    v.min(2, "insert_min"), // [!code focus]
  ]),
});
```

```ts title="insert-one.ts"
import { table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

export const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([
    v.onInsert(v.min(2, "insert_min")), // [!code focus]
    v.min(1, "title_required"),
  ]),
});
```

#### onUpdate

`v.onUpdate()` with no argument is a marker. Rules after it run only on update, until the next marker. Pass one rule and only that rule is limited to update. Rules after that call are not.

```ts title="update-phase.ts"
import { table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

export const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([
    v.min(1, "title_required"),
    v.onUpdate(), // [!code focus]
    v.min(4, "update_min"), // [!code focus]
  ]),
});
```

```ts title="update-min.ts"
import { table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

export const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate([v.onUpdate(v.min(4, "update_min"))]), // [!code focus]
});
```

#### Column checks

The column type adds a check when validation is on. You do not pass a rule for it. Guarded, generated, and identity columns are not checked.

```ts title="columns.ts"
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  title: t.varchar(8), // [!code focus]
  status: t.text().picklist(["draft", "active"]), // [!code focus]
  qty: t.integer(), // [!code focus]
  cost: t.numeric(8, 2), // [!code focus]
  score: t.double(), // [!code focus]
  token: t.uuid(), // [!code focus]
  meta: t.jsonb(), // [!code focus]
});
```

| Column | What fails | Issue |
| --- | --- | --- |
| required on insert, or null | not nullable, and the column can be written. A default lets insert omit it. Passing null on a not-null column fails too, including one with a default | `required` |
| `t.varchar(n)`, `t.char(n)` | longer than `n` code points | `too_long` |
| `.picklist(values)` | a value that is not one of the strings. `{ check: false }` skips the database CHECK and this check still runs | `picklist` |
| `t.smallint()`, `t.integer()`, `t.bigint()` | outside that type's range. A safe integer, a bigint, or a digit string is accepted | `integer_range` |
| `t.numeric(precision, scale)` | outside that precision and scale. A bare `t.numeric()` accepts a decimal | `precision` |
| `t.real()`, `t.double()` | a value that is not a finite number | `precision` |
| `t.uuid()` | a string that is not `8-4-4-4-12` hex | `uuid` |
| `t.json()`, `t.jsonb()` | a value JSON cannot store | `json` |

<Callout type="error" title="OKM1200">
A failed check throws. The text is `Validation failed:` then the path and the key. `issues` lists `{ path, message }` in column order, then `$row`. `message` is the key. A hidden or sensitive value is not written into the issue. A list prefixes each path with the row index.
</Callout>

### Standard Schema

`.validate()` takes one Standard Schema instead of a list. `~standard.version` is 1, and `~standard.validate` checks the value. Zod, Valibot, and any other library with that shape work. A result of `{ value }` replaces the field. Each issue message is kept, and a path on the issue is added after the field.

In the [section](#section) the schema sits inside that field's list.

```ts title="zod.ts" no-check="zod is installed by the application"
import { z } from "zod";
import { table, t } from "okmodel/pg";

export const notes = table("notes", {
  id: t.identity(),
  title: t.text().validate(z.string().trim().min(1)), // [!code focus]
});
```

```ts title="valibot.ts" no-check="valibot is installed by the application"
import * as v from "valibot";
import { table, t } from "okmodel/pg";

export const people = table("people", {
  id: t.identity(),
  email: t.text().validate(v.pipe(v.string(), v.email())), // [!code focus]
});
```

### Flag

`validation` overrides the [schema](/docs/define/schema/validation) for this table. A value that is not a boolean or `{ enabled, onRead, style }` is refused.

#### omitted

Leave it off the table. The table uses the schema flag, including `onRead` and `style`.

```ts title="inherit.ts"
import { schema, table, t } from "okmodel/pg";

const notes = table("notes", {
  id: t.identity(),
  title: t.text(),
});

export const app = schema({
  tables: [notes],
  validation: true, // [!code focus]
});
```

#### true

On for this table. `onRead` and `style` stay the schema's. `true` does not reset them.

```ts title="switch.ts"
import { table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  { validation: true }, // [!code focus]
);
```

#### false

Off for this table, even when the schema is on. `onRead` and `style` are still copied, and the rules do not run. A stored rule still makes a write demand `okmodel/validate`. That write is OKM1201 when the import is missing.

```ts title="off.ts"
import { table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  { validation: false }, // [!code focus]
);
```

#### enabled

Inside the object, omit `enabled` and the table is on. `true` is on. Any other value is off. `onRead` and `style` stay the schema's when you omit them.

```ts title="enabled.ts"
import { table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  { validation: { enabled: true } }, // [!code focus]
);
```

#### onRead

`true` is the only value that sets it. Omit it and the schema's value stays. The flag is stored. Insert and update run the rules. A read does not.

```ts title="on-read.ts"
import { table, t } from "okmodel/pg";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  { validation: { onRead: true } }, // [!code focus]
);
```

#### style

`"inline"` is [`.validate()`](#inline). `"section"` is the [`validate` option](#section). Anything else is refused: `validation.style must be inline or section.` Leave `style` unset and either place is allowed. The same field in both places is still OKM1030.

<Callout type="info" title="One style">
A column rule under `"section"`, or a section rule under `"inline"`, is OKM1030.
</Callout>

```ts title="section-style.ts"
import { table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

export const notes = table(
  "notes",
  {
    id: t.identity(),
    title: t.text(),
  },
  {
    validation: { style: "section" }, // [!code focus]
    validate: { title: [v.min(1, "title_required")] },
  },
);
```

### Run

Import `okmodel/validate`. A write that would validate without that import is OKM1201. `okm check` reports it too. These members exist on the table only when its validation is on.

#### insert.validate

`insert.validate(row)` checks the row and returns it frozen. A later `insert` of that value skips the rules. A list is checked the same way.

```ts title="run.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.varchar(200).validate([v.trim(), v.min(1, "title_required")]),
});
const app = schema({ tables: [notes], validation: true });

export async function add(url: string, title: string) {
  await using db = connect(url, { schema: app });
  const row = await db.notes.insert.validate({ title }); // [!code focus]
  return db.notes.insert(row);
}
```

#### insert.check

`insert.check(row)` runs the same checks and returns the issues. It does not throw OKM1200.

```ts title="check.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.varchar(200).validate([v.min(1, "title_required")]),
});
const app = schema({ tables: [notes], validation: true });

export async function issues(url: string, title: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert.check({ title }); // [!code focus]
}
```

#### pick

`insert.pick(...fields)` checks only the named fields. `validate` returns those fields frozen. `check` returns the issues.

```ts title="pick.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.varchar(200).validate([v.min(1, "title_required")]),
  body: t.text(),
});
const app = schema({ tables: [notes], validation: true });

export async function titleOnly(url: string, title: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert.pick("title").validate({ title }); // [!code focus]
}
```

#### omit

`insert.omit(...fields)` checks every field except those.

```ts title="omit.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.varchar(200).validate([v.min(1, "title_required")]),
  body: t.text(),
});
const app = schema({ tables: [notes], validation: true });

export async function withoutTitle(url: string, body: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert.omit("title").validate({ body }); // [!code focus]
}
```

#### update.validate

`update.validate` checks a patch. A body with `set` checks `set`. Update has no `check`, `pick`, or `omit`.

```ts title="rename.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.varchar(200).validate([v.min(1, "title_required")]),
});
const app = schema({ tables: [notes], validation: true });

export async function rename(url: string, title: string) {
  await using db = connect(url, { schema: app });
  return db.notes.update.validate({ set: { title } }); // [!code focus]
}
```

#### validate: false

`{ validate: false }` on insert or update skips the rules. Codecs still run. Delete ignores the option.

```ts title="skip.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { v } from "okmodel/validate";

const notes = table("notes", {
  id: t.identity(),
  title: t.varchar(200).validate([v.min(1, "title_required")]),
});
const app = schema({ tables: [notes], validation: true });

export async function raw(url: string, title: string) {
  await using db = connect(url, { schema: app });
  return db.notes.insert({ title }, { validate: false }); // [!code focus]
}
```


# Visibility (/docs/define/table/visibility)

<Callout type="info" title="Where this fits">
The SQL type stays. Hidden, sensitive, and guarded change the row the client sees and the fields a write may set. [Validation](/docs/define/table/validation) rejects a value the type still accepts.
</Callout>

None of the three flags is stored in the catalog, so none of them changes a migration.

### Hidden

`.hidden()` takes one option, `filterable`. Omit it and the column is off the row. `true` opens `where` and `orderBy` at runtime. The column stays off the row either way. Insert and update can still set it.

#### .hidden()

`select`, `where`, and `orderBy` take keys of the row, so a typed call cannot name the field. At runtime those two refuse it. A root `select` list that names it returns it. An include drops it silently, including when that list names it. `filters()` refuses it in `allow`, `sort`, and `relations`. `groupBy`, `bucket`, `sum`, `avg`, `min`, and `max` refuse it. Search refuses it.

```ts title="users.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const users = table("users", {
  id: t.identity(),
  email: t.text(),
  passwordHash: t.text().hidden(), // [!code focus]
});
const app = schema({ tables: [users] });

export async function register(url: string, email: string, passwordHash: string) {
  await using db = connect(url, { schema: app });
  const row = await db.users.insert({ email, passwordHash });
  return row.email;
}
```

<Callout type="error" title="OKM1120">
`where` and `orderBy` refuse the column. The fix says to call `hidden({ filterable: true })`. `groupBy`, `bucket`, `sum`, `avg`, `min`, `max`, and search refuse it too.
</Callout>

<Callout type="error" title="OKM1123">
`filters()` refuses the field in `allow`, `sort`, or `relations`. See [filters](/docs/define/table/filters).
</Callout>

<Callout type="info" title="Roles">
The column stays out of a reporting role's select unless [roles.columns](/docs/define/config/roles) names it.
</Callout>

#### filterable

`{ filterable: true }` is the only value that changes the flag. Omit it, or pass `false`, and `where` and `orderBy` stay refused. At runtime, `where`, `orderBy`, and search accept the column. It is still off the row, so a typed call cannot name it. `filters()`, `groupBy`, `bucket`, `sum`, `avg`, `min`, and `max` still refuse it. The reporting role still omits it.

`.nullable()` in the sample lets insert omit the field. `filterable` does not.

```ts title="reset.ts"
import { table, t } from "okmodel/pg";

export const users = table("users", {
  id: t.identity(),
  resetCode: t.text().nullable().hidden({ filterable: true }), // [!code focus]
});
```

### Sensitive

`.sensitive()` takes no argument. The column stays on the row, and a read returns the stored value. `inspect()`, logs, errors, and OpenTelemetry spans replace that value with `[redacted]` or omit it. A unique, foreign-key, or check failure on a table that has a sensitive column keeps the value out of `error.cause` as well: the cause carries only safe fields (`code`, `sqlstate`, `constraint`, `table`, column names), never `detail`, parameters, `where`, or `internalQuery`. Tables with no sensitive column keep today's cause. `connect({ errors: { includeValues: true } })` is the explicit opt-in that can put values on the error. A codec rejection says `Field token was rejected.` The same column can also be [hidden](#hidden).

```ts title="sessions.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const sessions = table("sessions", {
  id: t.identity(),
  token: t.text().sensitive(), // [!code focus]
});
const app = schema({ tables: [sessions] });

export async function token(url: string, id: string) {
  await using db = connect(url, { schema: app });
  const row = await db.sessions.one({ where: { id } });
  return row?.token;
}
```

### Guarded

`.guarded()` takes no argument. The write option is `allow`, on that call.

#### Closed

Insert and update omit the field. A value that arrives is OKM1190.

```ts title="accounts.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const accounts = table("accounts", {
  id: t.identity(),
  email: t.text(),
  role: t.text().guarded(), // [!code focus]
});
const app = schema({ tables: [accounts] });

export async function register(url: string, email: string) {
  await using db = connect(url, { schema: app });
  return db.accounts.insert({ email });
}
```

<Callout type="error" title="OKM1190">
Input cannot set a guarded field. Pass `{ allow }` with the field name, or leave it out.
</Callout>

#### allow

`{ allow: ["role"] }` opens `role` for that call. `allow` is a list of field names on the table. A value that is not a list, or a name the table does not have, is OKM1120.

```ts title="promote.ts"
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";

const accounts = table("accounts", {
  id: t.identity(),
  email: t.text(),
  role: t.text().guarded(),
});
const app = schema({ tables: [accounts] });

export async function promote(url: string, email: string) {
  await using db = connect(url, { schema: app });
  return db.accounts.insert({ email, role: "owner" }, { allow: ["role"] }); // [!code focus]
}
```

<Callout type="error" title="OKM1120">
`allow` must be a list of field names on the table.
</Callout>

#### Stays closed

A sealed trait field stays OKM1190. The message says the trait sets it. Insert of the [tenant key](/docs/define/schema/tenancy) is OKM1190. An update or a `where` on that key is OKM1704. `allow` does not open either.

<Callout type="info" title="Filled elsewhere">
[`timestamps()`](/docs/define/table/traits) and [`archivable()`](/docs/define/table/traits) mark their columns guarded and sealed. Input cannot set those columns.
</Callout>


# Backfill (/docs/define/config/backfill)

backfill sets batch size, pause, and batch statement timeout.

`batchSize` is written into a new plan. `pauseMs` and `statementTimeoutMs` are read when the batches run.

| Field | Required | Default | Role |
| --- | --- | --- | --- |
| `batchSize` | no | `1000` | Rows in `batch=` on a new plan. |
| `pauseMs` | no | `0` | Milliseconds after a committed batch, and not after the last one. |
| `statementTimeoutMs` | no | statement timeout, then `30000` | `statement_timeout` for one batch. |

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  backfill: { // [!code focus]
    batchSize: 1000,
    pauseMs: 50,
    statementTimeoutMs: 30_000,
  },
});
```

A new plan writes one header. It names the table, the key, and `batch=`. `pauseMs` and `statementTimeoutMs` stay out of the file.

```sql title="migrations/20260301000000_init.sql"
-- backfill table="public"."notes" key="id" batch=1000
```

Apply reads `batch=` from that line. Changing `batchSize` does not change a file that already exists.

Omit `statementTimeoutMs` and the batch uses the apply [statement](/docs/define/config/timeouts) timeout, then 30 seconds. The batch `lock_timeout` is the apply lock timeout.

<Callout type="error" title="Refused">
`batchSize` below 1, or not an integer, is refused: `backfill.batchSize must be an integer of at least 1.` A header whose `batch` is below 1 is refused.
</Callout>


# Database (/docs/define/config/database)

`database` is one target named `default`. `targets` replaces it. Set one of them. The name does not set protection. `NODE_ENV` is not read.

<Callout type="error" title="Both">
`Set database or targets, not both.`
</Callout>

| Field | Result |
| --- | --- |
| `database` | One target. Its name is `default`. A string is the URL, and `protected` is false. An object is `{ url, protected }`. |
| `targets` | Named targets, in sorted order. Replaces `database`. A string leaves `protected` false. |
| `url` | The connection string for that target. |
| `protected` | Optional. `true` blocks contract work. Omitted or `false` leaves the target open. |

| Name | Result |
| --- | --- |
| Any key | That string is the target name. `Dev` is not `dev`. |
| `database` | The name is `default`. Pass `--target default`. |
| A key named `dev` | `okm dev` uses that target. |
| No key named `dev` | `okm dev` opens PGlite at `<out>/dev-db`. The default `out` is `.okm`. |
| `targets: {}`, or neither field | No target is configured. |

| URL | Result |
| --- | --- |
| Missing or `""` | OKM1845. `Target <name> has no url.` The environment is not read. |
| Does not parse | OKM1845. `The target URL is not a Postgres URL.` A URL that parses is accepted. The fix asks for `postgres://`. |
| Host | Lowercased. |
| Port omitted | `5432`. |
| Path | The database name, decoded. It is not lowercased. |
| `primary`, `replicas`, `weight`, or `pool` on the object | OKM1845. `Target <name> is a replica. Migrations run on the primary only.` |
| Host contains `pooler` or `pgbouncer`, port `6543` or `6432`, or query `pgbouncer=true` | Refused when a command connects. `Refusing pooler host <host>. Apply needs a direct connection.` |
| `allowPooler: true` or `--allow-pooler` | That host check is skipped. |

`okm migrate apply`, `okm push` when it connects, `provision()`, `okm seed`, `okm migrate check`, and `okm check` when it compares a database run the host check. `okm migrate plan` and `okm migrate status` do not. Naming the URL does not refuse it.

`okm migrate apply`, `okm push` when it connects, `provision()`, and that same `okm check` comparison also read `pg_backend_pid()` twice. A change is OKM1854. The same opt-in skips the probe.

| Case | Result |
| --- | --- |
| One target | `--target` is optional. |
| Several targets and no `--target`, on `okm migrate apply`, `okm push`, `okm seed`, `okm migrate check`, `okm ext`, or `okm doctor` when `roles` is set | OKM1853. `Several targets are configured and --target was not passed.` |
| `--target` with no name | OKM1853. `--target needs a name.` |
| A name that is not configured | OKM1845. `Target <name> is not configured.` |
| No target, on those commands or on `okm migrate status` | OKM1845. `No target is configured.` |
| `okm migrate status` | Prints every target, in name order. The columns are `target`, `version`, `catalog`, `state`, and `protected` (`true` or `false`). `--target` keeps one name. Several targets do not need `--target`. |
| `okm check`, several targets, no `--target` | Skips the database comparison. An unknown `--target` is still OKM1845. A protection mismatch is still OKM1852. |
| `okm check` or `okm migrate plan`, no target | Does not throw OKM1845. Plan prints without a database. |
| `okm migrate plan`, several targets, no `--target` | Prints the plan. It does not throw OKM1853. A protection mismatch is still OKM1852. |

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!, // [!code focus]
});
```

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  targets: {
    dev: process.env.DEV_URL!,
    prod: { url: process.env.PROD_URL!, protected: true }, // [!code focus]
  },
});
```

<Callout type="error" title="OKM1845">
`No target is configured.` `Target <name> is not configured.` `Target <name> has no url.` `The target URL is not a Postgres URL.` `Target <name> is a replica. Migrations run on the primary only.`
</Callout>

<Callout type="error" title="OKM1853">
`Several targets are configured and --target was not passed.` `--target needs a name.`
</Callout>

<Callout type="error" title="OKM1854">
`The backend pid changed from <first> to <second>. A pooler is handing out a new session between statements.`
</Callout>

### Protection

`--allow-protected` is this invocation only. It does not change the config. `connect()` does not read it.

| Work | Protected target |
| --- | --- |
| `plan`, `status`, `check`, `drift`, `verify`, `pull`, `catalog-export`, `inspect` | Allowed. |
| `expand`, `reference`, `provision` | Allowed. A target that is not empty is still OKM1851. |
| `contract`, `unclassified`, `push`, `backfill`, `seed`, `history-repair` | OKM1850 until `--allow-protected`. |
| `okm migrate check` | OKM1850. `--allow-protected` does not lift it. Point it at a throwaway Postgres. |
| `drop`, `rollback` | Refused on every target. The code is `invalid`. `--allow-protected` does not lift it. |

`verify`, `pull`, `catalog-export`, and `inspect` have no command. `okm migrate plan` is `plan`. `okm migrate status` is `status`. `okm check` is `check` and `drift`. `okm doctor`, when `roles` is set, and `okm ext` are `check`. `okm push` is `push`. `okm seed` is `seed`. `provision()` is `provision`.

On `okm migrate apply`, the unit is `backfill` when it contains a backfill step. Otherwise the last `contract` or `unclassified` step sets the class. With neither, the unit is `expand`. A history repair rewrites a stored catalog hash. A protected target needs `--allow-protected` when a row changes.

```sh
okm migrate apply --target prod --allow-protected
```

<Callout type="error" title="OKM1850">
`<operation> is blocked on protected target <name>.` `okm migrate check` says `okm migrate check writes a scratch schema and refuses protected target <name>. Point it at a throwaway Postgres.`
</Callout>

<Callout type="error" title="No down migration">
`drop` and `rollback` throw `There is no down migration.` Recovery is a new forward migration.
</Callout>

### The same database

The match is the host, the port, and the database name. The user, the password, and the query string are left out. The host is lowercased, and an omitted port is `5432`, so those spellings match. The database name is not lowercased. `okm check` and `okm migrate plan` report a mismatch.

| Pair | Result |
| --- | --- |
| One `protected`, one not | OKM1852. `<open> and <protected> resolve to the same database and disagree about protection.` |
| Both unprotected | Allowed. They may share a database. |
| Both protected | Allowed. |

<Callout type="error" title="OKM1852">
`<open> and <protected> resolve to the same database and disagree about protection.`
</Callout>

### An empty database

`okm migrate apply` installs or replays when a head snapshot exists. With no migration files, or no head snapshot, it prints `nothing to apply` and does not connect. The check looks at the current schema. `okm_meta`, `okm_history`, and `okm_backfill` do not count as objects. A table, partitioned table, view, materialized view, sequence, foreign table, enum, domain, or extension does.

| State | `okm migrate apply` | `provision()` |
| --- | --- | --- |
| Empty | Installs the head snapshot and the [reference rows](/docs/define/table/reference-rows). The history id is `provisioned@` plus the migration id. | The same install. |
| History present | Replays the pending files, then inserts reference rows that are missing. | OKM1851. `Target <name> already has migration history and cannot be provisioned.` |
| Objects present, no history | OKM1851. `Target <name> is not empty (<object>) and has no migration history.` | OKM1851. The same message. |
| A provision that stopped part-way | OKM1851. `Target <name> is not empty (<object>): a previous provision stopped part-way; drop the schema or database and run again` | OKM1851. The same message. |

<Callout type="error" title="OKM1851">
`Target <name> is not empty (<object>) and has no migration history.` `Target <name> already has migration history and cannot be provisioned.` `Target <name> is not empty (<object>): a previous provision stopped part-way; drop the schema or database and run again`
</Callout>


# External (/docs/define/config/external)

`external` is a list of snapshot paths, one file per owning catalog. Omitted means this project owns every table it declares.

Each path is what `okm catalog export` wrote. `okm check` and `okm migrate check` compare every `external()` table with those files. A missing file, a table the files do not list, or columns that differ is OKM1841. When a target is selected, the same commands also compare the file with that database.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  namespace: "billing",
  external: ["../identity/identity.okm.json"], // [!code focus]
});
```

The declaration is [Several catalogs](/docs/define/schema/catalogs).


# Overview (/docs/define/config)

<Anatomy call="defineConfig" shape="object" exported>
  <AnatomyPart slot="schema" label="Schema" code='schema: "./schema.ts",' mark="required">
    [`schema`](/docs/define/config/schema) is required. `okm` loads this file.
  </AnatomyPart>
  <AnatomyPart slot="database" label="Database" code="database: process.env.DATABASE_URL!," mark="or targets">
    [`database`](/docs/define/config/database) is one target named `default`. `targets` replaces it. Set `database` or `targets`. Both is refused.
  </AnatomyPart>
  <AnatomyPart slot="sections" label="Pages" code="…," mark="sections">
    Each other field is a page in this group.
  </AnatomyPart>
</Anatomy>

`defineConfig` is the file `okm` reads. Each option is a page.

`okm` reads `okmodel.config.ts` from the project directory. The file default-exports `defineConfig` from `okmodel/migrate`.

<Callout type="error" title="Refused">
A missing `okmodel.config.ts` fails the import. An export that is not an object with a string `schema` is refused: `okmodel.config.ts must default-export defineConfig(...).`
</Callout>

<Callout type="info" title="Runtime">
Node.js 22.18 or newer loads the config. `bunx okm` uses Node. `bun --bun okm` uses Bun.
</Callout>

## The declaration

The list is how often you reach for each part while writing the file. `schema` and the target come first.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({ // [!code focus]
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
});
```

| Order | Page | You use it for |
| --- | --- | --- |
| 1 | [Schema](/docs/define/config/schema) | `schema` is required. `okm` loads this file. |
| 2 | [Database](/docs/define/config/database) | `database` is one target named `default`. `targets` replaces it. |
| 3 | [Tables](/docs/define/config/tables) | `tables` is a directory `okm check` reads for orphan table files. |
| 4 | [Migrations](/docs/define/config/migrations) | `migrations` is where `okm generate` writes SQL. |
| 5 | [Out](/docs/define/config/out) | `out` is where `okm build` writes the catalog. |
| 6 | [Timeouts](/docs/define/config/timeouts) | `timeouts` sets `lock_timeout` and `statement_timeout` for apply. |
| 7 | [Pooler](/docs/define/config/pooler) | `allowPooler` skips the pooler-host refusal. |
| 8 | [Lint from](/docs/define/config/lint-from) | `lintFrom` is the first migration id `okm migrate check` lints. |
| 9 | [Backfill](/docs/define/config/backfill) | `backfill` sets batch size, pause, and batch statement timeout. |
| 10 | [Roles](/docs/define/config/roles) | `roles` names migration, app, and optional reporting grants. |
| 11 | [Namespace](/docs/define/config/namespace) | `namespace` is the Postgres schema this catalog owns. |
| 12 | [External](/docs/define/config/external) | `external` lists catalog snapshots this project verifies. |


# Lint from (/docs/define/config/lint-from)

lintFrom is the first migration id okm migrate check lints. Files earlier in apply order are not linted. The id itself is linted.

Lint reads each SQL file in the [migrations](/docs/define/config/migrations) directory and reports findings. `lintFrom` chooses the first id in that pass.

| Value | Result |
| --- | --- |
| omitted | Every file. |
| a migration id | That file and every file after it, in apply order. |
| an id that is not a migration | Refused: `lintFrom <id> is not a migration id.` |
| an error finding on a linted file | OKM1510. |
| warnings only | The command continues. |

### omitted

Leave `lintFrom` out. `okm migrate check` lints every file.

### id

The id is the file name without `.sql`. `okm generate` writes a number of at least four digits, `_`, and the name. An empty directory and the name `init` produce `0001_init`.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  lintFrom: "0003_restore", // [!code focus]
});
```

`0003_restore` and every later file are linted. Earlier files are not. This skips lint only. The previous-catalog check, the head check, and the replay still cover every file.

### other commands

`okm migrate apply` does not read `lintFrom`. It lints every migration that still has a step to run, before any DDL. A database with no `okm_history` lints every file. No other command reads `lintFrom`.

<Callout type="error" title="OKM1510">
An error finding on a linted file fails `okm migrate check`. The message is one line per finding.
</Callout>


# Migrations (/docs/define/config/migrations)

`migrations` is where `okm generate` writes SQL. `okm migrate apply` reads the same directory.

| Value | Result |
| --- | --- |
| omitted | The directory `migrations`. |
| a string | That path, resolved from the project directory. |

### omitted

Leave `migrations` out. Generate writes a file such as `0001_init.sql`, using `\n` as the line ending, and writes `0001_init.catalog.json` beside it. The number is one higher than the highest numeric prefix in the directory, padded to four digits. With no prefix, the file starts at `0001`. An empty plan writes nothing in this directory.

### string

Set `migrations` and generate and apply use that directory.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  migrations: "migrations", // [!code focus]
});
```

Apply loads every `*.sql` file in name order, together with its `.catalog.json`. A SQL file without that snapshot is refused. For `0001_init.sql` the message is `Migration 0001_init is missing its catalog snapshot.`

`okm migrate check` reads this directory and runs the statements it is given. Run it as a non-superuser. The first migration id it lints is [`lintFrom`](/docs/define/config/lint-from).

A type change that rewrites the table is not one `ALTER COLUMN … TYPE`. It is five migrations: add the new column, backfill it, dual-write, switch reads, then drop the old column. See [type change](/docs/reference/type-change).

### Line endings

`okm generate init` writes `.gitattributes` in the project when that file is missing. An existing file stays as it is.

```text
migrations/*.sql text eol=lf
*.catalog.json text eol=lf
.okm/** text eol=lf
```

The first line is the literal pattern `migrations/*.sql`.

A migration file is read as LF. `\r\n` and a lone `\r` become `\n`, including inside a SQL string. A leading UTF-8 BOM is stripped.

<Callout type="error" title="UTF-16">
A null byte is refused. The message is the file path followed by `is UTF-16. Save the file as UTF-8.`
</Callout>


# Namespace (/docs/define/config/namespace)

`namespace` is the Postgres schema this catalog owns. Omitted means `public`.

`schema()` still compiles `public`. `okm` places the catalog into `namespace` before it plans. Apply creates that schema when it is not `public`, then sets the session search path so `okm_meta`, `okm_history`, and `okm_backfill` land there. `connect()` reads `okm_meta` in that namespace. `public` keeps the unqualified query.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  namespace: "identity", // [!code focus]
});
```

Two catalogs on one database use two configs and two values, for example `identity` and `billing`. The page for that layout is [Several catalogs](/docs/define/schema/catalogs).


# Out (/docs/define/config/out)

`out` is where `okm build` writes the catalog.

| Field | Required | Default | Role |
| --- | --- | --- | --- |
| `out` | no | `.okm` | Build output. |

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  out: "./.okm", // [!code focus]
  database: process.env.DATABASE_URL!,
});
```

Omit `out` and the directory is `.okm`. The directory is created when it is missing. A relative path is resolved from the project directory. An absolute path stays absolute.

`okm build` writes `catalog.json`, `catalog.hash`, and `types.d.ts`. `catalog.hash` is the SHA-256 of `catalog.json`. `types.d.ts` is the row types. `okm generate` writes the same three files.


# Pooler (/docs/define/config/pooler)

`allowPooler` skips the pooler-host refusal.

Apply holds an advisory lock on one session. A transaction-mode pooler can hand the next statement a different backend.

| Field | Required | Default | Role |
| --- | --- | --- | --- |
| `allowPooler` | no | `false` | Skip the refusal. `--allow-pooler` does the same for that invocation. Either one is enough. |

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  allowPooler: true, // [!code focus]
});
```

The host is lowercased before the check. The message is `Refusing pooler host <host>. Apply needs a direct connection.` The code is `invalid`.

| Shape | Result |
| --- | --- |
| Host contains `pooler` | Refused. |
| Host contains `pgbouncer` | Refused. |
| Port `6543` or `6432` | Refused. |
| Query `pgbouncer=true` | Refused by the migrate guard. Migrate strips that parameter before the URL becomes a startup parameter; other pooler hints are not interpreted. |
| A direct `postgres://` URL | Allowed. The flag is not required. |

| Command | Pooler host |
| --- | --- |
| `okm migrate apply`, `okm migrate check`, `okm push`, `okm seed` | Refused until `allowPooler` or `--allow-pooler`. |
| `okm check`, when it compares one database | Refused until `allowPooler` or `--allow-pooler`. |
| `provision()` | Refused until `allowPooler` in the config. The call takes no flag. |
| `okm migrate status`, `okm migrate plan`, `okm doctor`, `okm ext` | Not refused. |
| `connect()` | Not refused. |

<Callout type="warning" title="OKM1854">
Apply, push, seed, provision, and a one-database `okm check` read `pg_backend_pid()` eight times on the reserved session and hold another backend between reads so a LIFO pooler cannot reuse one server. `okm migrate check` does the same inside the scratch replay. The reads are not one transaction. Two or more distinct pids are OKM1854. The same opt-in that skips the host check skips the probe.
</Callout>

Connection pool size is [`max`](/docs/define/connect/session#max).


# Roles (/docs/define/config/roles)

`roles` names migration, app, and optional reporting grants.

A name in `managed` is a role the plan creates. A name outside that list is external and has to exist already. `schema()` has no `roles` field. The grants are added when the config sets `roles`.

| Field | Required | What it sets |
| --- | --- | --- |
| `migration` | yes | Runs the files. When this role already exists and is not the session user, apply issues `SET ROLE` before the statements. Default privileges are for this role. |
| `app` | yes | Receives the grants in the first table. After the plan, `SELECT` on `okm_meta`. |
| `reporting` | no | The grants in the second table. |
| `managed` | no | Roles the plan creates. See [managed](#managed). |
| `columns` | no | Column `SELECT` or `UPDATE`. See [columns](#columns). |

Grants are on managed objects.

| Object | `app` |
| --- | --- |
| table | `SELECT`, `INSERT`, `UPDATE`, `DELETE` |
| view | `SELECT`, `INSERT`, `UPDATE`, `DELETE` |
| materialized view | `SELECT` |
| sequence | `USAGE`, `SELECT` |
| function | `EXECUTE` |

Default privileges are `ALTER DEFAULT PRIVILEGES` for the migration role, in each schema those objects use. With no such schema, they are issued in `public`.

| Object | Privileges |
| --- | --- |
| tables | `SELECT`, `INSERT`, `UPDATE`, `DELETE` |
| functions | `EXECUTE` |
| sequences | `USAGE`, `SELECT` |

The plan revokes `INSERT`, `UPDATE`, and `DELETE` on a materialized view granted to `app`. After the plan, `app` receives `SELECT` on `okm_meta`. When this run creates the migration role, that role receives `USAGE` and `CREATE` on the schema, and `SELECT`, `INSERT`, and `UPDATE` on `okm_meta`, `okm_history`, and `okm_backfill`. Apply then issues `SET ROLE`.

| Object | `reporting` |
| --- | --- |
| table | `SELECT` |
| view | `SELECT` |
| materialized view | `SELECT` |
| schema | `USAGE` |

`reporting` also receives a default `SELECT` on later tables, for the migration role.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  roles: {
    migration: "okm_migrate", // [!code focus]
    app: "okm_app", // [!code focus]
    reporting: "okm_read", // [!code focus]
  },
});
```

### Managed

`managed` lists roles the plan creates and alters. A role is never dropped. The statement is `NOSUPERUSER NOCREATEDB NOCREATEROLE`. An external role emits no `CREATE ROLE` and no `ALTER ROLE`. `login` and `inherit` are altered only when the role is already managed.

| Field | Default | Result |
| --- | --- | --- |
| `name` | — | The role name. |
| `login` | `NOLOGIN` | `LOGIN` when `true`. |
| `inherit` | `INHERIT` | `NOINHERIT` when `false`. |

A managed role that `pg_roles` does not have is created. `okm doctor` skips that absence. The role that applies a `CREATE ROLE` or `ALTER ROLE` needs `CREATEROLE`. A superuser passes the same check. The message names that role: `Role postgres lacks CREATEROLE. Nothing was changed.` A role another session created first does not fail the create.

### Columns

`columns` grants `SELECT` or `UPDATE` on named columns. `on` is the SQL name of one table, view, or materialized view. A column may be the field name or the SQL name. An empty list adds no grant. A role named only here receives those column grants.

| Field | Required | What it sets |
| --- | --- | --- |
| `role` | yes | The role that receives the privilege. |
| `on` | yes | The table, view, or materialized view. |
| `select` | no | Columns granted `SELECT`. |
| `update` | no | Columns granted `UPDATE`. |

On a table with a [hidden](/docs/define/table/visibility#hidden) column, `reporting` receives `SELECT` on the visible columns. `hidden({ filterable: true })` stays in that hidden set. A `select` list for `reporting` adds those columns, including a hidden one. On a table with no hidden column, `reporting` receives `SELECT` on the table. On a view or a materialized view, a `select` list for `reporting` is the `SELECT` grant for those columns.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  roles: {
    migration: "okm_migrate",
    app: "okm_app",
    reporting: "okm_read",
    managed: [{ name: "okm_app", login: true }], // [!code focus]
    columns: [{ role: "okm_app", on: "users", select: ["email"] }], // [!code focus]
  },
});
```

| Case | Result |
| --- | --- |
| An empty `migration`, `app`, `reporting`, managed `name`, or column `role` | Refused. `roles.migration needs a name.` The label is `migration`, `app`, `reporting`, `managed role`, or `column grant role`. |
| `reporting` is `migration` or `app` | Refused. `Role okm_app cannot be the reporting role and also migration or app.` |
| `update` on the reporting role | Refused. `Role okm_read is the reporting role and cannot be granted update.` |
| `on` names an ignored table | Refused. `Column grant on users targets an ignored table.` |
| An external `migration`, `app`, or `reporting` role is absent | `okm doctor` refuses. `Role okm_app is named in roles and does not exist.` |
| `on` or a column is missing, or `on` matches more than one object | OKM1020. |
| A privilege is missing on an object that exists | OKM1825. An object absent from the database is skipped. |
| `connect()` | Leaves roles unchecked. |

<Callout type="error" title="OKM1020">
`Column grant names missing, which is not in the schema.` `Column users.missing is not in the schema.` `Column grant on users matches more than one object.`
</Callout>

<Callout type="error" title="OKM1825">
`Role okm_read lacks SELECT(email) on public.users.email.` `okm doctor` names the role, the privilege, and the object.
</Callout>

<Callout type="error" title="OKM1122">
A reserved word is `role user is a reserved word.` A name past 63 bytes includes the count, as in `is 64 bytes. The limit is 63.`
</Callout>


# Schema (/docs/define/config/schema)

`schema` is required. `okm` loads this file. `okm build` reads it and does not connect.

A value that is not a string refuses the config. The code is `invalid`. The message is `okmodel.config.ts must default-export defineConfig(...).`

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts", // [!code focus]
  database: process.env.DATABASE_URL!,
});
```

### path

A path is resolved from the project directory, the directory `okm` starts in. An absolute path stays absolute. A relative path, including `..`, is resolved from that directory.

### export

`okm` imports the file and reads its exports. A value with a `tables` array and `catalog` is the schema. [`schema()`](/docs/define/schema) returns that value. When several exports match, the name that sorts first is used.

A file with no such export is refused. The code is `invalid`. The message is `<path> must export a schema().` `<path>` is the `schema` string.

On that refusal, `okm migrate apply` still runs. It installs no reference rows.


# Tables (/docs/define/config/tables)

`tables` is a directory `okm check` reads for orphan table files. The name is the file name without `.ts`. That text has to appear in the [schema](/docs/define/schema) module source.

| Case | Result |
| --- | --- |
| omitted | The directory is not read. |
| a `.ts` name absent from the schema module | OKM1024. |
| not `.ts` | Skipped. |
| a name that starts with `.` | Skipped. |

`okm check` lists the files in that directory. A folder inside is not opened. A missing directory is an empty list.

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  tables: "./tables", // [!code focus]
  database: process.env.DATABASE_URL!,
});
```

<Callout type="error" title="OKM1024">
`Table file not in schema: extra.ts.` Add the file's table to the `schema()` call that `okm check` reads.
</Callout>


# Timeouts (/docs/define/config/timeouts)

`timeouts` sets `lock_timeout` and `statement_timeout` for apply. Both values are milliseconds. A flag overrides the config for that invocation. `0` disables the timeout. Connect ceilings are a separate [timeouts](/docs/define/connect/session#timeouts) object.

| Field | Required | Default | Role |
| --- | --- | --- | --- |
| `lock` | no | `5000` | `lock_timeout` when apply omits the field and the flag. `--lock-timeout` overrides it. |
| `statement` | no | `30000` | `statement_timeout` when apply omits the field and the flag. `--statement-timeout` overrides it. |

```ts title="okmodel.config.ts"
import { defineConfig } from "okmodel/migrate";

export default defineConfig({
  schema: "./schema.ts",
  database: process.env.DATABASE_URL!,
  timeouts: {
    lock: 2000, // [!code focus]
    statement: 15000, // [!code focus]
  },
});
```

`--lock-timeout` and `--statement-timeout` need a finite number of milliseconds that is not negative. A missing value fails the same way. The message is `--lock-timeout needs a number of milliseconds.` The same sentence names `--statement-timeout`.

### commands

| Command | `timeouts` | Flag |
| --- | --- | --- |
| `okm migrate apply` | Used when the flag is omitted. | `--lock-timeout`, `--statement-timeout`. |
| `okm migrate check` | Sets a field only when the config has it. Omit a field and that timeout stays unset. | Not applied. |
| `okm push`, `provision()` | Not read. Both stay `5000` and `30000`. | Not applied. |
| `okm check` | Not read. The command does not set either timeout. | Not applied. |

`okm check`, `okm push`, and `okm migrate check` still parse the flag, so a bad number is refused. The value is not applied. `provision()` takes no flag.

### session

Apply sets the two values on the session. `okm migrate check` sets them for its transaction.

A backfill batch sets `lock_timeout` to the apply lock timeout. It sets `statement_timeout` to `backfill.statementTimeoutMs` when that field is set, and to the apply `statement` timeout otherwise.
