@ultimat3/db 29.3.2 → 30.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. package/CLAUDE.md +78 -116
  2. package/README.md +199 -16
  3. package/package.json +13 -2
  4. package/src/app-relation.ts +7 -2
  5. package/src/catalog-fold.ts +5 -0
  6. package/src/catalog-objects.ts +20 -24
  7. package/src/catalog-policies.ts +81 -0
  8. package/src/catalog-relations.ts +31 -10
  9. package/src/catalog.ts +40 -1
  10. package/src/check-ddl.ts +34 -7
  11. package/src/client.ts +132 -48
  12. package/src/column-alter.ts +42 -1
  13. package/src/concurrent-index.ts +157 -0
  14. package/src/concurrent-plan.ts +294 -0
  15. package/src/connection-url.ts +16 -4
  16. package/src/db-diagnostics-deadline.ts +43 -0
  17. package/src/db-diagnostics-redact.ts +58 -0
  18. package/src/db-diagnostics-rows.ts +166 -0
  19. package/src/db-diagnostics-sql.ts +92 -0
  20. package/src/db-diagnostics.ts +178 -0
  21. package/src/destructive.ts +21 -35
  22. package/src/diagnostics-entry.ts +15 -0
  23. package/src/drift-errors.ts +9 -4
  24. package/src/drift-findings.ts +47 -62
  25. package/src/drift-missing.ts +203 -0
  26. package/src/drift-row-security.ts +88 -0
  27. package/src/drift-triggers.ts +54 -0
  28. package/src/drift.ts +88 -21
  29. package/src/entity-shape.ts +75 -0
  30. package/src/errors.ts +80 -9
  31. package/src/expected-loop.ts +34 -4
  32. package/src/failure-store-pg.ts +157 -0
  33. package/src/foreign-key-plan.ts +75 -23
  34. package/src/foreign-key.ts +76 -5
  35. package/src/generate-counter.ts +85 -0
  36. package/src/generate-history.ts +142 -0
  37. package/src/generate-row-security.ts +51 -0
  38. package/src/generate-snapshot.ts +82 -0
  39. package/src/generate-steps.ts +99 -0
  40. package/src/generate-triggers.ts +119 -0
  41. package/src/generate-view.ts +97 -0
  42. package/src/generate.ts +128 -89
  43. package/src/index-ddl.ts +23 -35
  44. package/src/index-plan.ts +35 -14
  45. package/src/index.ts +136 -6
  46. package/src/introspect-catalog.ts +26 -6
  47. package/src/introspect-rows.ts +82 -0
  48. package/src/introspect.ts +108 -50
  49. package/src/keyset.ts +182 -0
  50. package/src/lifecycle-errors.ts +15 -0
  51. package/src/local-settings.ts +38 -0
  52. package/src/marker-line.ts +47 -0
  53. package/src/migrate-lock-retry.ts +60 -0
  54. package/src/migrate-rollback.ts +45 -6
  55. package/src/migrate.ts +67 -69
  56. package/src/migration-errors.ts +48 -1
  57. package/src/migration-ledger.ts +38 -4
  58. package/src/migration-step.ts +192 -0
  59. package/src/partition-order.ts +56 -0
  60. package/src/partitioned-tables.ts +24 -0
  61. package/src/pool-gate.ts +120 -0
  62. package/src/pool-listeners.ts +143 -0
  63. package/src/pool-pins.ts +68 -0
  64. package/src/pool-profile.ts +23 -7
  65. package/src/pool-reserve.ts +11 -4
  66. package/src/pool-retire.ts +22 -0
  67. package/src/pool-sweep.ts +115 -0
  68. package/src/pool-timer.ts +10 -0
  69. package/src/pooler-fix.ts +53 -0
  70. package/src/pooler.ts +161 -0
  71. package/src/read-transaction.ts +59 -0
  72. package/src/readonly-query.ts +4 -4
  73. package/src/replica-client.ts +54 -22
  74. package/src/replica-position.ts +89 -0
  75. package/src/replica-scope.ts +25 -4
  76. package/src/retired.ts +131 -0
  77. package/src/retype-dependents.ts +2 -0
  78. package/src/rolling-errors.ts +97 -0
  79. package/src/rolling.ts +212 -0
  80. package/src/row-secure-client.ts +54 -0
  81. package/src/row-security-catalog.ts +26 -0
  82. package/src/row-security-migrator.ts +48 -0
  83. package/src/row-security-plan.ts +58 -0
  84. package/src/row-security-role.ts +60 -0
  85. package/src/row-security-roles.ts +64 -0
  86. package/src/row-security.ts +59 -0
  87. package/src/same-tenant.ts +173 -0
  88. package/src/schema-dump-policies.ts +26 -0
  89. package/src/schema-dump-table.ts +21 -6
  90. package/src/schema-dump.ts +13 -2
  91. package/src/snapshot-parse.ts +42 -2
  92. package/src/sqlstate.ts +4 -0
  93. package/src/statement-bound.ts +82 -0
  94. package/src/transaction-errors.ts +14 -0
  95. package/src/transaction-guard.ts +40 -0
  96. package/src/transaction-settings.ts +24 -0
  97. package/src/transaction.ts +74 -19
  98. package/src/ungeneratable.ts +30 -0
package/CLAUDE.md CHANGED
@@ -29,11 +29,7 @@ consults `currentTx()`; `withTransaction` uses `baseClient()`, never `db()`. Kee
29
29
 
30
30
  ## Connections and transactions
31
31
 
32
- - **`pglite.ts` is a pool of exactly one**; `reserve()` (`pglite-turns.ts`) serialises `BEGIN`s. Three
33
- rules: the plain path takes a turn; a statement inside a LIVE transaction on THIS client
34
- (`liveTxConnection()`, never `currentTx() !== undefined`) skips the queue; a reservation runs direct
35
- only while its turn is held. `pglite-embedded.test.ts`, `pglite.test.ts`,
36
- `pglite-two-clients.test.ts`, `pglite-observer.test.ts`.
32
+ - **`pglite.ts` is a pool of exactly one**; `reserve()` (`pglite-turns.ts`) serialises `BEGIN`s. Full rule: `docs/history/db.md`.
37
33
  - **The third rule is both drivers'**: `client.ts`'s pinned handle also runs direct only while held.
38
34
  `release()` is idempotent on both; `DbConnection` and `Turn` are `Disposable`.
39
35
  - **A pin is held by `using`, never a hand-rolled `try/finally`** (`withTransaction`,
@@ -48,36 +44,32 @@ consults `currentTx()`; `withTransaction` uses `baseClient()`, never `db()`. Kee
48
44
  - **Four codes are `retryable`** (`DB_ERROR_RETRY`); terminal ones stay unclassified
49
45
  (`errors-retry.test.ts`). Core's `retry()` executor is NOT adopted.
50
46
  - **`BEGIN` re-derives its isolation level from the closed set** (`isolationMode`; else `X_SQL_UNSAFE`).
51
- - **An aborted transaction is reported** (`X_DB_TRANSACTION_ABORTED`; README "Transactions that end
52
- badly"): a failure carrying a SQLSTATE marks the root's `abort`, only a successful `ROLLBACK TO`
53
- clears it (a failed one sets it), and `COMMIT` is then refused unsent. `commit-tag.ts` reads the
54
- COMMIT tag in BOTH funnels. `COMMIT` rejecting with no SQLSTATE is `X_DB_COMMIT_UNKNOWN`: neither
55
- list runs. `transaction-errors.ts`; `transaction-options.ts` holds `DbTx`, options, `BEGIN` text.
47
+ - **An aborted transaction is reported** (`X_DB_TRANSACTION_ABORTED`): `COMMIT` is then refused unsent; rejecting with no SQLSTATE is `X_DB_COMMIT_UNKNOWN`. Full rule: `docs/history/db.md`.
48
+ - **A finished scope refuses** (`transaction-guard.ts`): `X_DB_TRANSACTION_CLOSED` once `outcome` left `open`; a coded `abandonment()` at a statement, nested scope, `BEGIN`, `COMMIT`, unless `completeTransaction()` marked the root. **Never in `runOn`.** README.
56
49
  - **Sibling nested scopes take turns** (`TxState.children`; savepoints are a stack) under a deadline
57
50
  (`sibling-turn.ts`, `siblingWaitMs`, `X_DB_SIBLING_SCOPE_TIMEOUT`). A nested
58
51
  `isolation`/`readOnly`/`deferrable`/foreign `client` is `X_INVARIANT`.
59
- - **`close()` is BOUNDED by the driver's own `{ timeout }` in SECONDS** (`drainTimeoutMs / 1000`; `0`
60
- sends none); the verdict is elapsed `performance.now()` (`X_DB_DRAIN_TIMEOUT`). It clears the
61
- cached driver before awaiting the teardown. `pool-drain{,.live}.test.ts`.
62
- - **`client.listen` is ONE session beside the pool** (`listen.ts`; `Bun.SQL.listen`, PGlite's
63
- `listen` under a turn), never a reserved pin. `onListening` fires on every re-dial; a channel is
64
- refused unless it is a plain identifier. `listen.test.ts`, `listen.live.test.ts`.
65
- - `execute()` trusts the command tag only when `> 0`, in both drivers (`rowsOf`, `affectedBy`).
66
- - **`pool-gauge.ts` derives `db_pool_max` / `db_pool_in_use` / `db_pool_waiting` from DEMAND**
67
- (`Bun.SQL` publishes no occupancy). `client.ts` is the one counter: `run()` from send to settle, a
68
- pin from ask to release; a statement ON a pin is not counted again. Declared on the first pool.
52
+ - **`close()` is BOUNDED by the driver's own `{ timeout }` in SECONDS** (`X_DB_DRAIN_TIMEOUT`). Full rule: `docs/history/db.md`.
53
+ - **`dbClientInstalled()` reads `installed`, a flag only `setDbClient` writes**, never `ambient`, which
54
+ the lazy default also fills: a unit test that sent one statement through `db()` must not flip
55
+ entity's `testDriver()` onto a pool nobody asked for.
56
+ - **`client.listen` is ONE session beside the pool** (`listen.ts`), never a reserved pin. Full rule: `docs/history/db.md`.
57
+ - **`pool-gauge.ts` derives `db_pool_*` from DEMAND** (`Bun.SQL` publishes no occupancy); `client.ts`
58
+ is the one counter. Full rule: `docs/history/db.md`.
69
59
  - **`client.ts` connects, holds the client and the ambient `db()`**, and opens no socket at import;
70
- `pool-profile.ts`, `connection-url.ts`, `bun-sql.ts`, `pool-reserve.ts`, `db-health.ts` (`checkDb`)
71
- and `statement-funnel.ts` (`sendOn`/`runOn`) hold the rest.
60
+ `pool-profile.ts`, `connection-url.ts`, `bun-sql.ts`, `pool-reserve.ts`, `db-health.ts` (`checkDb`),
61
+ `statement-funnel.ts` (`sendOn`/`runOn`), `pool-gate.ts`, `pool-sweep.ts`, `pool-retire.ts`,
62
+ `pool-listeners.ts` and `pool-timer.ts` hold the rest.
63
+ - **`pool-gate.ts` is the one count of `max`** (`takeSlot`/`freeSlot`), per pool; a wait past `acquireTimeoutMs` is `X_DB_POOL_EXHAUSTED`. Full rule: `docs/history/db.md`.
64
+ - **`pool-sweep.ts`: an unanswered statement retires the pool** (`statementTimeoutMs + UNANSWERED_GRACE_MS`), pooled or pinned; age is ticks that fired, never a clock. Full rule: `docs/history/db.md`.
65
+ - **`pool-pins.ts`: a pin is swept against ITS bound** (a `statement_timeout` it set; the role's after `COMMIT`/`ROLLBACK`); after a miss it sends nothing. `reserve()` passes `reserveWithin` what the gate left of one budget. **`pool-listeners.ts` holds `client.listen`** and re-dials every session on a retirement, with backoff.
66
+ - **`pooler.ts` owns the pooler**: `postgresClient({ env })` is the one reader; everything after asks `client.pooler`. Never a detection. `pooler.live.test.ts`. Full rule: `docs/history/db.md`.
67
+ - **`idleInTransactionMs` is sent beside `statement_timeout`**; a pooler refusing it is `X_DB_POOLER_OPTIONS_REFUSED`. Full rule: `docs/history/db.md`.
72
68
  - **`libpq-options.ts` merges the framework's `options` into the operator's**: the framework wins
73
69
  on the names it sets, the operator keeps the rest; emitted for all six roles.
74
70
  - **`DATABASE_URL`'s scheme is screened at boot** (`POSTGRES_SCHEMES`: `postgres:`, `postgresql:`); the
75
71
  received scheme is never echoed (it may be a credential).
76
- - **A JS array bound as a parameter is rendered here** (`array-parameter.ts`, `bound-parameters.ts`,
77
- called only by `sendOn`) — `Bun.SQL` joins elements with commas. `NULL` bare vs `"NULL"`; quoting by
78
- content; a `Uint8Array` is BYTEA, in an array too; a ragged nest and an Invalid Date are refused
79
- (`X_INVARIANT`), ABOVE the driver's `try` in both funnels. `array-parameter.live.test.ts`;
80
- `packages/cli/src/pg-array.live.test.ts` is the composition test.
72
+ - **A JS array bound as a parameter is rendered here** (`array-parameter.ts`, `bound-parameters.ts`, called only by `sendOn`) — `Bun.SQL` joins elements with commas. Full rule: `docs/history/db.md`.
81
73
  - **Every numeric option is screened** through core's `finiteCount` (`replicaClient`'s breaker,
82
74
  `migrate`'s `lockWaitMs`, `readonlyQuery`'s `timeoutMs` — only an explicit `0` disables it — the pool
83
75
  profile, `reapBranches`' `maxAgeMs`).
@@ -100,6 +92,10 @@ consults `currentTx()`; `withTransaction` uses `baseClient()`, never `db()`. Kee
100
92
  - **`expected-loop.ts` is the ONLY suppression**: `expectedQueryLoop(reason, fn)`, innermost reason,
101
93
  blank is `X_INVARIANT`; the funnel stamps `expected`; it suppresses a verdict, never a statement.
102
94
  The framework's own loops declare themselves (`migrate()`, `rollback()`, admin's `search.ts`).
95
+ Sibling: `expectedSeqScan` (plan sweep's exemption).
96
+ - **`failure-store-pg.ts` writes `x_failures` through `outsideTransaction`** — the 5xx rolled the
97
+ request's transaction back. Table applied by the boot (`FRAMEWORK_SCHEMA`) whether capture is on
98
+ or not; bundles only (counts are `x_error_groups`'). `failurePurgeTarget` has no default window.
103
99
  - `@ultimat3/jobs` never imports this package; its statements pass the observer through
104
100
  `dbExecutor` (`db-executor.ts`, the ONE `PgExecutor` builder; the CLI binds it), unattributed.
105
101
 
@@ -109,49 +105,30 @@ consults `currentTx()`; `withTransaction` uses `baseClient()`, never `db()`. Kee
109
105
  `MIGRATION_LOCK_WAIT_MS`, then `X_MIGRATE_CONCURRENT`), declared with `expectedQueryLoop`;
110
106
  `recordingClient` stubs the lock as `locked: true`.
111
107
  - **`lock_timeout` is the migration's** (`SET LOCAL` inside each migration's transaction, from the
112
- `migrate` profile's 3 s).
108
+ `migrate` profile's 3 s). `migration-step.ts` applies a script.
109
+ - **`-- transaction: false` runs a migration outside a transaction** (`concurrent-index.ts`); a
110
+ concurrent statement in a transactional one is `X_MIGRATION_CONCURRENT_IN_TRANSACTION`. Full rule: `docs/history/db.md`.
111
+ - **`migrate-lock-retry.ts` sends again on `X_DB_LOCK_TIMEOUT`**. Full rule: `docs/history/db.md`.
112
+ - **`rolling.ts` decides WHAT a previous release trips over** (a closed list); `rolling-errors.ts` holds every `X_MIGRATION_ROLLING_UNSAFE`. Full rule: `docs/history/db.md`.
113
113
  - **The advisory lock is held by one pinned session, and `migrate()`/`rollback()` run every statement
114
114
  on it.** `lock: false` reserves nothing and takes no lock; no shipped path passes it
115
115
  (`migrate-pin.test.ts`). `migrate.live.test.ts` pins concurrent and failed-midway runs.
116
- - **One send is one statement**: `applyScript` sends `statementsOf(script)` one at a time inside the
117
- same transaction. **`statement-split.ts` is the only splitter** (a left-to-right scan; `$1` is never a
118
- `$tag$`; `\` escapes only inside `E''`; comment-only chunks dropped). **`sql-scan.ts` is the one
119
- lexer** (`noiseAt`, source order; a `$tag$` needs separating from the identifier before it);
120
- `sql-noise.ts` holds `stripSqlNoise`.
116
+ - **One send is one statement**: `applyScript` sends `statementsOf(script)` one at a time. Full rule: `docs/history/db.md`.
121
117
  - **`destructive.ts` decides WHAT is destructive**: only `up`, a closed list of four (`drop table`,
122
118
  `drop column`, `truncate`, `alter column … type`), decided on blanked text and reported on the
123
- original; the `-- destructive: true` marker is a top-level line comment (`hasDestructiveMarker`
124
- walks `sql-scan.ts`). `X_MIGRATION_DESTRUCTIVE` (ship) and `X_MIGRATION_IRREVERSIBLE` (generate) are
119
+ original; a marker is a top-level line comment (`marker-line.ts`). `X_MIGRATION_DESTRUCTIVE` (ship) and `X_MIGRATION_IRREVERSIBLE` (generate) are
125
120
  two questions.
126
121
  - **The ledger audit asks one question** — `auditLedger`'s `foreign` filter is `!known.has(row.id)`;
127
122
  the app version lives in the cause.
128
- - **`rollback({ steps })` refuses anything but a positive safe integer**, before the lock
129
- (`rollbackStepsInvalid`, `X_INVARIANT`).
130
- - **`refuseDependentViews(tx, script)`** (`dependent-view.ts`) runs before each migration's first
131
- statement: a word scan over `sql-scan.ts` finds retyped columns, one catalog round trip, the pair
132
- filtered in JS (visible tables only), and `X_MIGRATION_VIEW_DEPENDS` carries the `drop view` /
133
- `create view` from `pg_get_viewdef` (built through `identifier()` inside a `try`).
134
- - `runningAppVersion()` delegates to core's `appVersion()`.
123
+ - **`refuseDependentViews(tx, script)`** (`dependent-view.ts`) runs before each migration's first statement. Full rule: `docs/history/db.md`.
135
124
 
136
125
  ## Generation (`x db gen`)
137
126
 
138
127
  - **`generate.ts` reads an index, never re-derives one** — `IndexDescriptionLike` carries columns,
139
128
  unique, `where`, `order`, `using`; an index naming no column is `X_INVARIANT`.
140
- - **An index's ACCESS METHOD is carried end to end** (`index-method.ts`): closed at `btree` and `gin`;
141
- declared is CLOSED, live is OPEN (`indexMethodOf` passes the catalog through; `declaredMethod`
142
- refuses); absent is `btree` through one function; `snapshotOf` records `using` only when declared;
143
- `indexMethodSql` re-derives the literal (`X_SQL_UNSAFE` default); a unique or ordered GIN is
144
- `X_INVARIANT`. `introspectSchema()` reads `pg_am` (`introspect-embedded.test.ts`).
145
- - **`index-plan.ts` walks both directions** (declared first, removed last). `dropRecordedIndex` emits
146
- `alter table … drop constraint if exists` then `drop index` for a shape a constraint could back
147
- (`mayBeConstraintBacked`); four names are skipped (primary, moved aside, rebuilt, over a dropped
148
- column). `index-ddl.ts` writes the statements; `index-removal.live.test.ts`.
149
- - **An entity's INVARIANTS reach the DDL** (`invariant-ddl.ts`): a `check` is a named constraint, a
150
- `unique` a unique INDEX on the one `declaredIndexes` list, an `assert` nothing; `checks` is recorded
151
- absent-never-`[]`; the name `<table>_<name>_<check|key>` is re-derived, bounded at 63 bytes and
152
- validated (`constraintNameUnsafe`). **An `assert` is an unrendered loss when a migration recorded its
153
- CHECK**: `unrenderedOf(entities, current)` takes the recorded schema (required, nullable) and
154
- `namesConstraint` matches both spellings.
129
+ - **An index's ACCESS METHOD is carried end to end**. Full rule: `docs/history/db.md`.
130
+ - **`index-plan.ts` walks both directions**; a removal and a `concurrently: true` build on a live table go to the non-transactional step (`concurrent-plan.ts`). Full rule: `docs/history/db.md`.
131
+ - **An entity's INVARIANTS reach the DDL** (`invariant-ddl.ts`): `check` a named constraint, `unique` a unique index, `assert` nothing. Full rule: `docs/history/db.md`.
155
132
  - **A COLUMN's CHECK is a named constraint too** (`check-ddl.ts`): one list (`declaredChecks` =
156
133
  `columnChecks` then `invariantChecks`), named `<table>_<column>_check` (Postgres' own name); an add on
157
134
  an existing column is `drop constraint if exists` then `add constraint`; two declarations naming one
@@ -162,32 +139,29 @@ consults `currentTx()`; `withTransaction` uses `baseClient()`, never `db()`. Kee
162
139
  - **`literal()` DOES receive caller input** (`column-default.ts`); `E'…'` only with a backslash, so
163
140
  every migration on disk stays byte-identical (`generate-default.live.test.ts`, `sql.test.ts`).
164
141
  `readonly-role.ts` and `branch.ts` are safe only by their ordering after `identifier()`.
165
- - **A retype moves its dependents aside** (`retype-dependents.ts`): only expressions that MENTION the
166
- column (partial-index predicates, CHECKs), over-approximated on purpose (`referencesColumn` walks
167
- `sql-scan.ts`); a plain btree survives. `generate-retype.live.test.ts`. What is moved is put back by
168
- the ordinary diff (`MovedAside`), never twice. **Foreign keys over a retyped column**
169
- (`retype-keys.ts`): the retype set is derived once for the whole schema (`retypedColumns`), drops go
170
- in `preAlters` at the top of `up`, re-adding is `foreignKeyPlan`'s, both `breaksOn` ends are needed
171
- (`generate-retype-key.live.test.ts`). `sql-type.ts` reads `SQL_TYPES` with `Object.hasOwn`.
172
- - **A generated column** (`generated-column.ts`): the clause right after the type; generated-and-
173
- defaulted refused; an expression change is `set expression as (…)`; a retype has no `using`; a NOT
174
- NULL add is one statement; generated → plain is `drop expression`; plain → generated rebuilds
175
- (`rebuilt`) and moves dependents aside, its own type change does not. `introspectSchema` never reads
176
- `generation_expression`.
177
- `generate-generated-{column,rebuild}.live.test.ts`.
142
+ - **A retype spans its default** (`retypeOverDefault`): `42804` otherwise.
143
+ - **A retype moves its dependents aside** (`retype-dependents.ts`, `retype-keys.ts`): expressions mentioning the column and foreign keys over it; the ordinary diff puts them back, never twice. Full rule: `docs/history/db.md`.
144
+ - **A generated column** (`generated-column.ts`): plain → generated rebuilds (`rebuilt`); the rest of
145
+ its rules are in `docs/history/db.md` (2026-10-31). `generate-generated-{column,rebuild}.live.test.ts`.
178
146
  - **`REPLICA IDENTITY FULL` is a PARAMETER** (`GenerateOptions.replicaIdentityFull`, from the CLI's
179
147
  `db-generate.ts`; `replica-identity.ts`): recorded `true` or absent, as a union; last in `up`; an
180
148
  undeclared name skipped; `down` reverts it unless this migration created the table.
181
- - **`appendOnly: true` is a trigger** (`generate-append-only.ts`): one `create or replace` function
182
- (SQLSTATE `23001`, message leads `X_ENTITY_APPEND_ONLY`) and a fixed-name `ultimate_append_only`
183
- `before update or delete … for each row`; recorded `true` or absent; no `truncate`. Drift:
184
- enabled `triggerNames` (`drift-append-only.ts`, `X_APPEND_ONLY_TRIGGER_MISSING`).
185
- - **A foreign key is `alter table … add constraint`**, collected into a bucket merged after every table
186
- statement (`foreign-key-plan.ts`); **dropping a table has its own bucket emitted BEFORE the table
187
- statements** (`preDrops`), ordered children-first by `drop-order.ts`, which breaks a two-table cycle
188
- by dropping one key first. `foreignKeyPlan` walks both directions, drops the name the previous
189
- snapshot recorded, and rebuilds a key whose `onDelete` moved. **`on delete` reaches the SQL**
190
- (`onDeleteRule`, `foreign-key.ts`; an unknown rule is `X_INVARIANT`).
149
+ - **`appendOnly: true` is a trigger** (`generate-append-only.ts`, `ultimate_append_only`); drift is
150
+ `X_APPEND_ONLY_TRIGGER_MISSING` (`drift-append-only.ts`). Full rule: `docs/history/db.md`.
151
+ - **A foreign key is `alter table … add constraint`** after every table statement (`foreign-key-plan.ts`); a table drop's bucket runs first, children-first (`drop-order.ts`). Full rule: `docs/history/db.md`.
152
+ - **`same-tenant.ts`**: `sameTenant` is `foreignKeysOf`'s composite; `withSameTenantIndexes` opens `generateMigration` and `snapshotOf`, idempotent. `set null (a)` is the rule's spelling everywhere (`introspectSchema()` off `pg_get_constraintdef`, never `confdelsetcols`). `foreign-key-plan.test.ts`. Why: `docs/history/db.md` (2026-11-02).
153
+ - **The deferred step holds keys too** (`concurrent-plan.ts`, phases in `stepStatements`: index builds, `keys`, `validate`, `contractKeys`, `contract`; `down` reversed: restore old key, drop new key, drop index). Full rule: `docs/history/db.md`.
154
+ - **Lifecycle triggers are one list per table** (`generate-triggers.ts`: `declaredTriggers`,
155
+ `triggerPlan`, `recordTriggers`): `history` → `ultimate_history` + `ultimate_history_guard`
156
+ (`generate-history.ts`), each `countedIn` → `ultimate_count_<crc32>` on the CHILD
157
+ (`generate-counter.ts`). Triggers, not the repository: only a trigger sees `updateWhere`, a cascade
158
+ and raw SQL. A history image stores `HistoryDescriptionLike.text` (`int8`/`numeric`) as digits and
159
+ `.instants` (`timestamptz`) as UTC ISO ms, as memory does (`generate-history-values.live.test.ts`).
160
+ Recorded on the snapshot; a NEW counter is recounted in the same migration. Drift:
161
+ `drift-triggers.ts` (`X_LIFECYCLE_TRIGGER_MISSING`; `retain` on a partitioned table is
162
+ `X_ENTITY_RETAIN_INVALID`).
163
+ - **Views**: `generate-view.ts`.
164
+ - **`retired.ts`: a retired column**. Full rule: `docs/history/db.md`.
191
165
  - **`entity-shape.ts` holds the `*Like` interfaces** (every later field optional).
192
166
  - **`snapshot-json.ts` writes a fixed point of Biome** (an array collapses when it fits at `<= 100`
193
167
  with its trailing comma); `snapshot-json.test.ts` runs `biome format`.
@@ -216,30 +190,29 @@ consults `currentTx()`; `withTransaction` uses `baseClient()`, never `db()`. Kee
216
190
  together (`unnest(a, b) with ordinality`), pinned by `introspect-embedded.test.ts`.
217
191
  - **`appTables()`** excludes all of `x_` for drift; `introspectSchema()` alone excludes
218
192
  `x_migrations` by default. **`app-relation.ts`**: `nonAppRelations(client, schema)` — extension
219
- ownership from `pg_depend` (`deptype = 'e'`) plus views, materialised views and foreign tables —
193
+ ownership (`pg_depend`, `deptype = 'e'`), views, materialised views, foreign tables, partitions —
220
194
  merged into `excluded` unconditionally.
221
- - **`unexpectedTable`'s `fix:` never names `x db gen`**: `psql -c '\d "T"'`, commented with the two
222
- repairs (claim it with `create table if not exists`, or drop it).
223
- - **`dbDrift()` lives in `drift-errors.ts`** (it needs `shellInertIdentifier`) and is the only one —
224
- `@ultimat3/entity`'s copy is deleted. `errors.ts` registers `DB_ERROR_TITLES` unconditionally.
225
- - `drift-findings.ts` holds every `DriftDifference` constructor and `DriftKind`; `drift.ts` keeps the
226
- comparisons.
195
+ - **A partitioned parent is a table, a partition is not**; a concurrent build on one is
196
+ `X_MIGRATION_CONCURRENT_PARTITIONED`. `docs/history/db.md` (2026-11-06).
197
+ - **`dbDrift()` lives in `drift-errors.ts`** (it needs `shellInertIdentifier`), the only one.
198
+ `errors.ts` registers `DB_ERROR_TITLES` unconditionally.
199
+ - **`not-valid-constraint`** (`compareValidated`): `notValidNames` is catalog-only (`convalidated`, absent when none), judged on the declared side, foreign keys by where they point; the fix is the validate statement (`repair`).
200
+ - `drift-findings.ts` holds the `DriftDifference` constructors and `DriftKind`; `drift-missing.ts` the
201
+ missing/rebuilt ones (table, column, index, foreign key); `drift.ts` keeps the comparisons. **No
202
+ drift fix is `x db migrate`**: the ledger holds the migration, so it applies nothing — every fix is a
203
+ `repair()` from the snapshot or `byHand()`, run and re-checked by `drift-repair.test.ts`.
227
204
 
228
205
  ## The schema dump
229
206
 
230
207
  - **Its own entry: `@ultimat3/db/schema-dump`** (`schema-dump-entry.ts`). The barrel is in every
231
208
  role's boot graph and must evaluate none of this family — `schema-dump-entry.test.ts`.
232
- - **Two readings of one catalog, never mixed.** `introspectSchema()` → `SchemaDescription`, the entity
233
- vocabulary a snapshot is diffed in. `introspectCatalog()` (`catalog.ts`; queries: `catalog-relations.ts`,
234
- `catalog-objects.ts`; pure fold: `catalog-fold.ts`) → `CatalogDescription`, Postgres' own `pg_get_*def` text, compared only to
235
- itself. A catalog spelling on a `SchemaDescription` field is the `checks`/`checkNames` mistake.
209
+ - **Two readings of one catalog, never mixed.** `introspectSchema()` → `SchemaDescription`, the entity vocabulary a snapshot is diffed in. Full rule: `docs/history/db.md`.
236
210
  - **Sorted in JS** (`byCodeUnit`), never `order by` and never `localeCompare`: collations differ.
237
211
  - **`renderSchemaDump()` is pure**; `schema-dump-table.ts` spells one table. `quoted()` escapes any
238
212
  catalog name — `identifier()` refuses whitespace and `"`, which a migration may have created.
239
213
  - **`x_` owner → `framework/` twin** (`FRAMEWORK_TABLE_PREFIX`). No list is handed in.
240
- - **What cannot be rendered is named** (`unrenderedRows`, a closed list → `unrendered.sql`; a trigger
241
- on an unrendered relation is named by the fold). A kind rendered later leaves that list in the
242
- same diff. Never render a partition as a plain table. `generated` carries `stored`/`virtual`.
214
+ - **What cannot be rendered is named** (`unrenderedRows` → `unrendered.sql`); never render a
215
+ partition as a plain table (`partition-order.ts`). Full rule: `docs/history/db.md`.
243
216
  - **`loadSchemaDump()`**: one transaction, `check_function_bodies` off, a savepoint per file, and
244
217
  only `42P01`/`42883`/`42704` are retried. Its refusal is `X_SCHEMA_DUMP_DRIFT` (`dump-drift.ts`).
245
218
  - **`object-drift.ts`**: `unexpectedObjects(live, expected)` compares IDENTITY (kind, table, name,
@@ -247,34 +220,23 @@ consults `currentTx()`; `withTransaction` uses `baseClient()`, never `db()`. Kee
247
220
  - **No app path here.** Where the dump lives, which engine replays, and when it is written are
248
221
  `@ultimat3/cli`'s (`db-schema-dump.ts`). `schema-load.contract.test.ts` reads the reference app's
249
222
  migrations as text and runs on Postgres when `TEST_DATABASE_URL` is set.
250
- - **`pglite-extensions.ts` is the one linker** (`linkPgliteExtensions` → `{ linked, missing }`):
251
- `contrib/<name>` then the package root; the name is data (read from migration text), screened
252
- by `pgliteExtensionExport` before it reaches a specifier; `plpgsql` is built in, never missing.
253
- Only a not-found import is `missing`; a bundle that throws is `X_DB_UNAVAILABLE`.
254
- - **`pglite-snapshot.ts`**: `snapshotDir` makes a `memory://` boot a restore. Key = PGlite version
255
- (unreadable = no cache), never the extension set. One file, checksum in its header; unsound or
256
- unopenable → deleted and rebuilt; unreadable → a miss. Temp name + `rename`. Uncompressed.
257
- - **Embedded boots for dump tests**: `schema-dump.test.ts` and `schema-dump-fidelity.test.ts`.
223
+ - **`pglite-extensions.ts` is the one linker** (`linkPgliteExtensions`); a name read from migration
224
+ text is screened by `pgliteExtensionExport` before it reaches a specifier. Full rule: `docs/history/db.md`.
225
+ - **`pglite-snapshot.ts`**: `snapshotDir` makes a `memory://` boot a restore, keyed by PGlite version. Full rule: `docs/history/db.md`.
258
226
 
259
227
  ## Branches, replicas, read-only
260
228
 
261
229
  - **`reapBranches` sweeps branches of THIS database**: the marker is `ultimate:branch:<base>:<iso>`,
262
230
  split on the ISO tail; an older one-segment marker or an unparseable `createdAt` is skipped, never
263
231
  dropped. `@ultimat3/cli`'s `ls`/`drop` scope by name prefix.
264
- - **Read replicas are opt-in twice**: a pool when `DATABASE_REPLICA_URL` names one
265
- (`default-client.ts`), and a read offered only inside `withReplicaReads(fn)` (`replica-scope.ts`);
266
- read-your-writes is `ReplicaScope.wrote`, never a request-id map. **`withTransaction` is on the
267
- primary structurally** (`replicatedClient` delegates `reserve()`; `isReservable` answers about the
268
- database); `runRoot` calls `markScopeWrote()` unless `readOnly`. **`isPlainRead` is an allow-list**,
269
- never `statementKind()` (`replica-route.test.ts` asserts they disagree). A standby refusal (`25006`)
270
- re-runs on the primary; the breaker (3 failures, 10 s, `Clock.monotonic()`) parks the replica;
271
- `ReplicaStats`; `db.replica_fallback`. The URL must name a read-only standby — nothing checks it.
272
- `@ultimat3/cli`'s boot opens the per-request scope.
273
- - **This package owns no "is this SQL a write?" lexer** — `readonly.ts` and `X_READONLY_VIOLATION`
274
- are deleted; `errors.test.ts` pins `DB_OWNED_ERROR_CODES`. The layers are `ensureReadOnlyRole()`
275
- (layer 1, returns `null` on a missing permission) and `readOnlyQuery()` (layer 2: `BEGIN READ ONLY`
276
- + statement timeout; it THROWS), with `@ultimat3/mcp` as layers 3–4. **`readOnlyQuery` takes ONE
277
- statement** (`multipleStatements`, via `statementsOf`) and splices the splitter's `statements[0]`.
232
+ - **Read replicas are opt-in twice**: a pool when `DATABASE_REPLICA_URL` names one (`default-client.ts`), and a read only inside `withReplicaReads(fn)` (`replica-scope.ts`). `withTransaction` stays on the primary; `isPlainRead` is an allow-list; the breaker is core's (`db.replica`). Full rule: `docs/history/db.md`.
233
+ - **Read-your-writes is one comparison** (`replica-position.ts`): `pg_current_wal_insert_lsn()`
234
+ after the write, never `pg_current_wal_lsn()` (it trails under `synchronous_commit = off`), vs the
235
+ standby's `pg_last_wal_replay_lsn()`, sampled once per `POSITION_CACHE_MS`. Unknown = primary.
236
+ - **This package owns no "is this SQL a write?" lexer.** Layers: `ensureReadOnlyRole()`, `readOnlyQuery()` (one statement; it throws), then `@ultimat3/mcp`. Full rule: `docs/history/db.md`.
237
+ - **`db-diagnostics.ts` is `dbDiagnostics()`, on its own entry `@ultimat3/db/diagnostics`**: facts,
238
+ never a verdict (`checkDb` stays `/readyz`'s); a section fails alone; no statement reads a user
239
+ table. Full rule: `docs/history/db.md`.
278
240
 
279
241
  ```bash
280
242
  bun test # from packages/db