# Changelog 0.7

## v0.7.0 — 2026-10-15

### ✨ Added

#### dialects

- `minPostgres: 17` sets the oldest Postgres major this schema runs on. The majors are 15, 16, 17, and 18. Omitted means 15. `requires: { postgres: ">=17" }` stores the same floor. The floor is not part of the catalog hash (D261).

#### runtime

- `bucket(column, unit, { tz, as })` groups an aggregate by a calendar or clock unit. A day or longer returns `Temporal.PlainDate`. On a `timestamptz`, a second, minute, or hour returns `Temporal.Instant`. Those three units are refused on a `date`. A NULL timestamp is one group: the result key is present and the value is null. `__proto__`, `constructor`, `toString`, and `hasOwnProperty` are refused as `as`, and so is an `as` that matches another bucket. There is still no `having`. Time-zone date helpers stay deferred (D247, D257).

- `update` with `lock: "skip"` and `limit` claims up to that many unlocked rows in one statement. A read still uses `lock: "update"` and `wait: "skip"` inside `tx()` (D249).

- `find({ search })` and `count({ search })` match a column built for search. A string is enough when the table has one. Rows come back closest first unless you pass `orderBy`. `page()` does not take `search`. A `language` that is not text is refused. An empty or malformed language name is OKM1122. A well-formed name is sent to the server (D250).

- `relevance(query, { match, language })` orders a search column by the match. `matches` takes the same options. `{ mode, config }` is not accepted (D250).

- A to-many `include` with `orderBy` and `limit` returns the first N related rows of each parent. The primary key breaks ties (D251).

- `rowNumber`, `rank`, `denseRank`, and `runningTotal` go in `select` on `find` or `one`. A running total is the column's type, or null. `page()` refuses a window when the call runs (D251).

- `descendants(id)` and `ancestors(id)` read the rows below or above a row in one statement. Pass `relation` when the self-relations use different columns. The fix lists the names. A row already on the path is not entered again. `ltree` is not required (D252).

- `sync(id, relation, ids)` sets a to-many relation to exactly those ids and reports the links it added and removed (D252).

- `versioned()` adds `version`. An update passes the version you read and the write bumps it. A stale write is a conflict and changes nothing. Leaving `version` out is refused, and so is `version` on a table that is not versioned (D254).

- `sortable(groupBy?)` adds `position` and `move(id, { before })` or `{ after }`. New rows go at the end of the group. A move is O(n) per group and rewrites every row in the group. Naming the group column in `set` is refused. `find` and `page` order by `position` when you omit `orderBy` (D254).

- `morph("commentable", ["tasks", "lists"])` includes the matching target, with the type set to that table's name, or null. `morphMany` is the reverse many. `morphTo` sets the type and the id together. Setting one without the other is OKM1121. A type outside the list is OKM1120 (D255).

#### docs

- The handbook documents those calls, including what each one refuses. Define is Schema, Connect, and Config. Start is Let's start and Basic Usage. Table is a group: one page per part of `table()`, from the key through the queries. Keys, Checks, Presets, and Reference rows are split into subsections.

- Traits shows `trait(name, { fields })`. `touch` seals those columns and sets them to `now()` on update. `methods` and `requires` stay OKM1061. Each built-in trait shows the call: insert for timestamps, `archive` and `restore` for archivable, `version` on update, and `move`.

- Defaults shows what insert does with each form. A literal and `.defaultSql()` are stored and applied when the field is omitted. A generator runs in the application and stays out of the catalog. `t.id()` and `t.identity()` are omitted from input. `.default(null)` follows `.nullable()`. A generated column is `stored`.

- Shape shows the value you pass. An array element cannot be null. A picklist CHECK is `column IN (...)`, and `{ check: false }` still rejects a value at encode. Adding an enum label in order plans `ALTER TYPE … ADD VALUE`. Removing or reordering a label replaces the type.

- Names: insert and `where` use the field name. `sqlName` is what the statement quotes, and casing does not rewrite a name you set. `renamedFrom` plans `RENAME`. A comment is checked and left off the catalog.

- Visibility splits the options. `.hidden()` leaves the column off the row. `filterable: true` opens `where` and `orderBy` at runtime. `.sensitive()` takes no argument and redacts logs, errors, and `inspect()` as `[redacted]`. `.guarded()` stays closed until `{ allow }`. A sealed field and a tenant key stay closed. A tenant key update is OKM1704.

- Validation splits the options, and each option has a sample. `v.trim()` and `v.lowercase()` take no argument and change the stored string. `v.email`, `v.min`, and `v.rule` take an issue key. A bare `onInsert()` or `onUpdate()` limits the rules that follow. Passing one rule limits that rule and leaves the rest of the list alone. `validation: true` keeps the schema's `onRead` and `style`. `false` turns the table off. `insert.validate` returns a frozen row. `insert.check` returns the issues. `pick` and `omit` choose the fields. `{ validate: false }` skips that call.

- References splits each action. The constraint is `{table}_{column}_fkey`. A new table adds it with `alter table`. A table that already exists adds it `not valid`, then validates it. `onDelete` and `onUpdate` take `cascade`, `no action`, `restrict`, `set default`, or `set null`, and both can be set together. `set null` needs a nullable column. On a tenant table, `set null` and `set default` are refused. Omitted `columns` uses a one-column primary key. One name selects another column, which has to be the primary key or a unique.

- Relations names the include. `one` and `many` take `field` when more than one foreign key matches. A to-many include needs `limit`. `has`, `none`, and `every` filter a relation. `manyThrough` takes `through`, `from`, and `to`. `morph` uses `${name}Type` and `${name}Id`. Include is `true`. `morphTo` sets both columns.

- Indexes take column handles in order. One column, a column order, and several indexes are separate calls. The name is `{table}_{columns}_idx` and follows a column or table `sqlName` and snake casing. A new table uses `create index`. A table that already exists uses `create index concurrently`, outside a transaction. `.unique()` can sit beside a plain index and does not create a unique constraint.

- Checks take a name key and `sql` text. The constraint is `{table}_{key}_check`. The key is not recased. A handle is inserted as its SQL name. A number is inserted as text. Quotes around a string stay in the template. A new table uses `alter table`. A table that already exists adds the check `not valid`, then validates it. A failed row is `Check violation` with kind `check`.

- `filters().parse` returns `{ where, orderBy }` for `find`. Each operator is its own section. A scalar is equality. Several operators become `and`. `in` and `inList` become `inList`. `sort` is ascending. A relation field becomes `has`, and a column of the same name wins. A simple request is one field and a sort. A complex request combines operators, a relation, and `sort`, then spreads into `find`. A hidden field is OKM1123 when `filters()` is called.

- `trait()` takes `fields`, `touch`, `sealed`, and `presets`. A touched field is sealed. Schema traits apply first. `omitDefaults` drops them and needs a reason. A repeated column is OKM1012. A simple trait adds one field. A complex trait adds a touched column, a sealed column, and a preset.

- Tenancy adds a guarded uuid. `for()` sets it. Input cannot. A unique includes the key unless `.unique({ global })`. `global("reason")` leaves the column off. `via()` follows up to three to-one relations and does not store the key. A global table cannot reference a tenant table.

- A preset is a `where` you call on the table. A field, an argument, a list, a bound, `or`, and two `where` calls are separate sections. A chain keeps the caller's `where` first. Update and delete still need their own `where`. Insert does not use the preset. A reserved name is OKM1040. A trait preset sees the trait's columns.

- Required rows are the `reference` option. They are rows the application expects, such as statuses. Apply inserts a missing key and does not update or delete. They are not a foreign key and not a migration insert.

- Queries add `page`, a grouped `aggregate`, a string `search`, both tree walks, a list `update`, and `inc`. A window `as` that clashes with a column is OKM1120. `page()` still reads `select` as field names.

- Each table option is its own subsection with a sample. `t.id()` shows `uuidv7`, `uuidv4`, a client generator, and `none`. A tenant unique leaves the key out with `{ global: "reason" }`. `has`, `none`, and `every` each filter a relation. `onConflict` is `"ignore"`, `{ on, update }`, or `{ on, return: true }`.

- Schema is a group under Define. Each option is a page: Tables, Casing, Codecs, Row types, Id default, Minimum Postgres, Traits, Tenancy, Validation, Extensions, Functions, Triggers, and Views. `/docs/define/schema` is the overview. `/docs/define/schema/requires` redirects to Minimum Postgres.

- Tables is the list `schema()` compiles. A reference names a table in that list, earlier or later. The same name twice, two names that emit one type, or one SQL name twice is OKM1023. A missing name is OKM1020. Swapping the list does not change the catalog hash.

- Casing `"snake"` rewrites a camelCase table or column. `publishedAt` is `published_at`. `userID` is `user_i_d`. `sqlName` and `renamedFrom` stay as written. Any other value is `schema() casing … must be snake.`

- Codecs choose the TypeScript value of `t.bigint()`, `t.identity()`, and `t.numeric()`. The SQL type stays. `"string"` is a decimal string. `"number"` is a safe integer for bigint and can lose digits on numeric. `"bigint"` is a JavaScript bigint. A schema codec other than `"string"` needs the same `as` on every column of that type. Timestamps stay `"temporal"`.

- Row types store `"emitted"` or `"inferred"`. Omitting the field is `"emitted"`. Queries still infer from `schema()`. `okm build` writes `.okm/types.d.ts` for either value. `notes` becomes `Notes`, `NotesInsert`, and `NotesUpdate`. A `t.jsonb<T>()` argument is copied from the schema source. Any other value is `schema() types … must be emitted or inferred.`

- Id default replaces a bare `t.id()`. Omitted and `"uuidv4"` store `gen_random_uuid()`. `"uuidv7"` stores `uuidv7()` and needs Postgres 18. A client generator is filled on insert and stays out of the catalog. `okid()` is `text` with collation `C`. A column `default` wins. `"none"` is that column option. A Postgres floor below 18 on `uuidv7()` is OKM1812.

- Minimum Postgres is `minPostgres`, a major from 15 through 18. Omitted means 15. `requires: { postgres: ">=17" }` stores the same floor and is deprecated. A bare `t.id()` stores `gen_random_uuid()`. An explicit `uuidv7()` on a floor below 18 is OKM1812. A server below the floor is OKM1802.

- Traits is a list applied before a table's own traits. An omitted list and `[]` add nothing. `omitDefaults` drops the schema list and needs a reason. A repeated column is OKM1012. `timestamps({ enforce: "trigger" })` adds `okm_touch_<column>` and a before-update trigger.

- Tenancy is `columnTenancy()`, `compositeTenancy()`, or `rlsTenancy()`. Omitted, a table that sets tenancy is OKM1060. Each key is a guarded uuid filled by `for()`. `rlsTenancy` keeps the column predicate and refuses the owner, a superuser, and `BYPASSRLS` (OKM1707).

- Validation is stored as given. `schema()` does not run the rules. Omitted and `false` are off. `true` and an object are on. `style` is `inline` or `section`. A field in both places is OKM1030. A write without `okmodel/validate` is OKM1201.

- Extensions is a list of `citext()`, `pgTrgm()`, or `extension("name")`. The record is contributed when the catalog is read. An omitted `citext` column is OKM1020. A list that names another extension is OKM1810. The same name twice is OKM1813. An exact version is written. A `>=` floor is stored and is not written.

- Functions is a list of `fn()`. plpgsql needs `dependsOn` (OKM1824). `security: "definer"` needs `searchPath` (OKM1823). `LANGUAGE sql` that begins with `BEGIN ATOMIC` can omit `dependsOn`. Volatility defaults to `volatile`. Security defaults to `invoker`.

- Triggers is a list of `trigger()`. Identity is the table plus the name. The function must already be in the catalog (OKM1020). `updateOf` is SQL column names and needs an update event at level row. Events are stored as insert, update, delete, truncate.

- Views is a list of `view()` and `materializedView()`. Output columns are text. A tenant view that hides the key is OKM1820. `refresh: "concurrently"` needs a unique index (OKM1822). Creation is `WITH NO DATA`. `db.views.name.refresh()` refreshes a materialized view.

- The Table sidebar leads with columns, keys, defaults, references, relations, and queries. Names, visibility, checks, and required rows come last.

- Docs pages drop the breadcrumb. Copy page sits on the title row and opens Markdown and chat actions. A code block focuses the line the paragraph names.

- Connect is a group under Define: Drivers, Target, Topology, Check, Session, Errors, Generators, and Observers. `/docs/define/connect` is the overview, including `close()`. Session is the pool size, prepare mode, TLS, search path, and deadlines. Check includes the catalog artifact and `requireMeta`.

- Define Config is one page per `defineConfig` option. `/docs/define/config` is the overview.

- The handbook adds Reference and AI Resources. Reference is CLI, Errors, OKID, and Environment Variables. AI Resources is MCP, Skills, and LLM. Retired `/docs/reference/known-limits` and `/docs/reference/linter` still redirect to the docs home.

- The site tells agents where to read. Every response links `/llms.txt`, the sitemap, and `/.well-known/agent-skills/index.json`. A handbook URL also links its markdown page. `robots.txt` allows search, live AI input, and training. The published skill is `okmodel`. A browser with WebMCP gets `search_docs`, `get_page`, and `list_pages`.

- The home page is a hero over one stage with twenty-one real examples in three groups. Migrate shows eight schema edits (add a column, require a value, add a unique key, add a foreign key, add an index, rename a column, add a picklist, drop a table), each with the `schema.ts` diff, the planned migration with class and lock, the lint findings, and the type change. Model shows six (timestamps, soft delete, enum, view, a tenant column, row-level security). Query shows seven calls (a relation include, a cursor page, full-text search, an aggregate, a tenant-scoped read, archived rows, a hidden column), each with the SQL it plans, the rows it returns, and the rules `inspect()` lists. Everything is real output, from `okm generate` and from the client on PGlite, regenerated by `site/scripts/hero-derived.ts`, and a test fails when the checked-in output drifts. Previous and next arrows, a counter, and a progress line walk the examples, the stage has a maximum height and each side scrolls when its content is taller, and the prism behind the copy takes the tint of the shown one.

- Extensions, functions, triggers, and traits each end with Create your own. A name outside `citext()` and `pgTrgm()` is `extension(name)`. A function is `fn()`. A trigger is `trigger()`. A schema trait is `trait(name, { fields })` on the schema list.

### 💥 Breaking Changes

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

### 🔥 Removed

- Safety, Drivers, and Known limits are no longer their own pages. The reference pages are not published. `/docs/known-limits` redirects to the docs home.

- Queries is no longer its own page. `/docs/start/queries` redirects to the Queries page under Table. Table is no longer one page. `/docs/define/table` is the overview, and each section is its own page.

- Handbook pages no longer end with a Next link.

### 🐛 Fixed

#### runtime

- `insert` with `onConflict: { on, return: true }` returns the existing row when another transaction inserts it first, and does not change that row (D246).

- An update list of two different timestamps, dates, or byte values is not the same target (D246).

- `update` with `lock: "skip"` matches the physical row by `tableoid` and `ctid`, so a claim does not change another partition or an inheritance child that shares the `ctid`. A path-parent update uses the same pair (D249).

- Concurrent inserts into one `sortable()` group no longer pick the same `position`. The advisory lock is its own statement, and the max is read after that lock is held (D254).

- `bucket()` refuses `as: "__proto__"`, `constructor`, `toString`, and `hasOwnProperty` before it writes the row (D257).

- `rowNumber`, `rank`, `denseRank`, and `runningTotal` refuse `__proto__`, `constructor`, `toString`, and `hasOwnProperty` as the result name. The error is OKM1120, names the key, and is raised before any SQL (D258).

- `find` and `count` with an explicit `search: undefined` are OKM1120, the same as `""` and `null`. A call that omits `search` is unchanged (D259).

- `bucket()`, `rowNumber`, `rank`, `denseRank`, and `runningTotal` refuse the rest of the `Object.prototype` names, and `then`, as the result key. The error is the same OKM1120 sentence (D259).

- `sync` on a tree-shaped `many` refuses an id list that contains the parent's own id and writes nothing. The message says "tree". `manyThrough`, including a row that links to itself, is accepted. Other relations are unchanged (D259).

- A malformed advanced search is OKM1120 and names the search input. A missing text search configuration is OKM1122, including `public.english`. Other syntax errors stay `driver` (D260).

- `morphTo` with a null name, a null type, or an id that is not a non-empty string is OKM1120. An empty string name or type stays OKM1020 (D260).

- Updating a tree parent to the row's own id is OKM1120 when `where` is the primary key. A filter that is not the key is not checked (D260).

- `update` with `limit` and `returning: true` returns the updated rows and honors `select`. Without it the result stays `{ count }` (D260).

- `update` with `{ allow, returning: true }` is typed as the updated rows, and that is what the call returns.

- `delete` with `{ returning: true }` is OKM1120 and deletes nothing. The option type does not accept `true`. Pass the field names, or omit `returning` for `{ count }`.

- An include `select` that names a hidden column is OKM1120. A default include still omits the column (D260).

#### dialects

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

#### adapters

- A server error whose text contains `CONNECT` is no longer treated as a lost connection. Classification uses the driver `code` and SQLSTATE (`08` and `57P01`–`57P03`) only. A live session is rolled back and released. A session that is gone is released without a rollback (D244).

- On PGlite, a JS number, bigint, or boolean bound to a column is sent as wire text. A parameter protocol error (`08P01`) is kind `driver`. A closed connection stays `unavailable` (D259).

- Bun.sql drops a pooled connection on a closed signal or `57P01` before the next statement uses it. A read that failed because that connection was already dead is tried once on a new connection. A write, a lock, or a statement inside a transaction is not retried (D260).

- Bun.sql rejects a wire integer outside the signed int8 range as kind `invalid`, including an id of 2^64. A numeric value written as that integer is refused too (D260).

- PGlite `connect(pool)` then `close()` marks that client closed. Later calls are `unavailable`. The pool stays open (D260).

- A reserved Bun.sql handle whose backend was killed fails the next statement with `unavailable` (SQLSTATE `08006`, retryable) after the pool drains the closed socket. A scoped read does not come back empty (D263).

- On postgres.js, a wait with no call deadline is rejected when every slot in its pool has closed, or when the reserved connection it uses closes. A queued `reserve` is started again after a close, because the driver drops that `sql.reserve()` without settling. A wait that already has a deadline is not on the close hook (D263).

- The timestamp conformance statements set the session time zone to UTC, so the wire text does not depend on the server time zone.

#### tooling

- `okm migrate check` does not open a second connection to drop the scratch schema when the replay rolls back (D245).

- The replica probe test has a 15 second budget, so a loaded CI runner no longer fails it on the default 5 second limit.

- `bun run attw` runs `node_modules/.bin/attw` from the repository root and fails with a clear error when that binary is missing.

- `bun run pack-tracked` packs the tarball and fails when a file in it, other than `dist/`, is not tracked by git. `bun run check` runs it. The release publish job runs it with `--release`, which also fails when the worktree is dirty.

- The docs site hero and theme control use inline SVG icons again so the client shell does not import `lucide-react`.

- Streaming replication waits poll for 45 seconds before `routing.auto` fails on a slow catch-up.

- The postgres.js adapter incremental gate is 16,000 minified bytes. The room is the close rule (D263).

#### docs

- The docs site favicon is the OKM mark: a white letter on a dark rounded square. It cycles O, K, then M on its own icon link, so opening a page does not rewrite the static icon.

- The favicon morph keeps the letter visible. The paths sit in the same box as the OKEngine mark, so the blur does not wipe the tile.

- The Schema page lists extensions as `citext()`, `pgTrgm()`, or `extension("name")`. Config states each option once.

- Text search: `natural` combines unquoted words with AND. `advanced` is raw `to_tsquery`. A syntax error in that query is OKM1120 and names the search input. A missing text search configuration is OKM1122. An empty or malformed name is still OKM1122 when the statement is built (D259, D260).

- A claim returns `{ count }` unless `returning: true`, which returns the updated rows, including when `{ allow }` names a guarded field. `delete` does not accept `{ returning: true }` (OKM1120, and the row stays). The handbook sets a `claimedBy` token, then `find`s by that token (D259, D260).

- PGlite is a pool of one. A statement issued while a `tx()` is open waits until it ends, unless `timeouts.acquire` is set (D259).

- A morph write can store another tenant's id. The include reads as the caller, so that target is null (D259).

- An include `select` that names a hidden column is OKM1120. A default include still omits it (D260).

- `okm migrate check` runs the statements it is given. Run it as a non-superuser (D259).

- The first call after a backend kill on an idle connection is `unavailable` and retryable. Bun.sql retries a client read once at transaction depth 0. postgres.js and node-postgres do not. A kill during a postgres.js connect can leave an unhandled rejection from the driver's type-catalog query (D263).

- A postgres.js wait with no call deadline is rejected on a close only when every slot in that pool has closed, or when the reserved connection it uses closes. A queued `sql.reserve()` is dropped by the driver without settling when a slot closes. okmodel starts it again. A wait that already has a deadline ends at that deadline (D263).

- Docs search answers `GET /api/search?query=` with the matching pages. An empty query and a query with no matches return an empty list. The static index stays on the server.

- A trigger that also fires on insert names `new` in `when`. Postgres rejects `old` on an insert event.

- `extension("hstore", { schema: "extensions" })` needs `create schema "extensions"` before apply. The column type is `extensions.hstore`, because that type is not on the search path.

- The traits insert is awaited. `await using` closes the client when the function returns, so a returned promise closed it before the row was written.

- The docs site reads the repository `package.json` by walking up to the manifest named `okmodel`. A Next output manifest with a trailing comma is not that file.

### ♻️ Changed

#### dialects

- A `uuidv7()` default on a floor below 18 is OKM1812 and names that major. A server below `minPostgres` is OKM1802. A server below 15 with no floor is OKM1803. The handbook page is Minimum Postgres (D261).

- A bare `t.id()` stores `gen_random_uuid()`, so it works on Postgres 15 to 18. `defaults: { id: "uuidv7" }` keeps the old database default. An explicit `uuidv7()` on a server below 18 is still OKM1812 and needs `minPostgres: 18`. An existing table whose default is `uuidv7()` plans `SET DEFAULT gen_random_uuid()` and does not rewrite the table (D262).

#### runtime

- `update` of a list refuses the same target twice (OKM1120). A target that matches nothing is not an error. An empty `set` is OKM1120. An empty list runs no statement (D246).

- `inc` is a number or bigint column. A text column is OKM1124. A list that sets one field with `inc` in one row and a plain value in another is OKM1120. A null number stays null (D249).

- `.stream()` yields rows in keyset batches on every driver, including PGlite. The default batch is 1000. `limit` is the total. A table with no primary key, `one()`, `count()`, `exists()`, `search`, and a window are refused. A positive `timeout` is the budget for the whole stream, including on a driver that cannot cancel. A row committed while it runs can be seen or missed (D256).

#### adapters

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

#### tooling

- The size gates for the featureless app and the connect entries measure the static startup graph. Dynamic import targets are empty modules in that build (D248).

- The docs site has no sync step. `site/content/docs` is the handbook and is checked in. Each page carries its title and description in frontmatter, and each folder's `meta.json` sets the sidebar order. `site/handbook`, `scripts/sync-docs.ts`, and `lib/docs-map.ts` are removed. The site check now verifies that every page is listed in its `meta.json`.

- The docs site deploys to production only after a release is published. A push to `main` no longer updates the public site, so the site shows the published version.

- The docs site publishes when the workflow moves the `release` branch to the release commit. Vercel builds that branch. The workflow no longer calls the Vercel CLI, and the repository no longer needs `VERCEL_TOKEN`, `VERCEL_ORG_ID`, or `VERCEL_PROJECT_ID`.

- A release requires the commit that was QA'd. The tag must point at that commit, `package.json` there must match the tag, and the published provenance must record that same commit.

#### docs

- The docs site uses IBM Plex Sans.

- The site header shows the OKM wordmark. The home link is named OKModel.

- The header wordmark is one step smaller. The theme control uses the Lucide moon and sun.

- The home page hero reads "Edit your schema. Get a safe migration." with a pitch for the migration, lint, and types the stage shows. The package description is unchanged.

- Known limits record the lost-connection release path and that a sortable move is O(n) per group.

- The changelog page shows `## Unreleased` as next until the release.

- The published README is the short page: the logo, install, a schema, `okm push`, and basic usage. The full walkthrough is `docs/guide.md`. A README image has to be an absolute `https://` URL. The docs site is `https://okm.omqkhafi.dev`.

- The spec status names the 0.7.0 release. Decisions run through D264.

### ⚠️ Deprecated

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