@ultimat3/db 29.4.0 → 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.
- package/CLAUDE.md +74 -110
- package/README.md +163 -15
- package/package.json +13 -2
- package/src/app-relation.ts +7 -2
- package/src/catalog-fold.ts +5 -0
- package/src/catalog-objects.ts +20 -24
- package/src/catalog-policies.ts +81 -0
- package/src/catalog-relations.ts +31 -10
- package/src/catalog.ts +40 -1
- package/src/check-ddl.ts +34 -7
- package/src/client.ts +132 -48
- package/src/column-alter.ts +42 -1
- package/src/concurrent-index.ts +20 -0
- package/src/concurrent-plan.ts +135 -17
- package/src/connection-url.ts +16 -4
- package/src/db-diagnostics-deadline.ts +43 -0
- package/src/db-diagnostics-redact.ts +58 -0
- package/src/db-diagnostics-rows.ts +166 -0
- package/src/db-diagnostics-sql.ts +92 -0
- package/src/db-diagnostics.ts +178 -0
- package/src/destructive.ts +17 -3
- package/src/diagnostics-entry.ts +15 -0
- package/src/drift-errors.ts +9 -4
- package/src/drift-findings.ts +47 -62
- package/src/drift-missing.ts +203 -0
- package/src/drift-row-security.ts +88 -0
- package/src/drift-triggers.ts +54 -0
- package/src/drift.ts +88 -21
- package/src/entity-shape.ts +67 -0
- package/src/errors.ts +77 -9
- package/src/expected-loop.ts +34 -4
- package/src/failure-store-pg.ts +157 -0
- package/src/foreign-key-plan.ts +75 -23
- package/src/foreign-key.ts +76 -5
- package/src/generate-counter.ts +85 -0
- package/src/generate-history.ts +142 -0
- package/src/generate-row-security.ts +51 -0
- package/src/generate-snapshot.ts +82 -0
- package/src/generate-steps.ts +6 -1
- package/src/generate-triggers.ts +119 -0
- package/src/generate-view.ts +97 -0
- package/src/generate.ts +105 -79
- package/src/index.ts +129 -6
- package/src/introspect-catalog.ts +26 -6
- package/src/introspect-rows.ts +82 -0
- package/src/introspect.ts +108 -50
- package/src/keyset.ts +182 -0
- package/src/lifecycle-errors.ts +15 -0
- package/src/local-settings.ts +38 -0
- package/src/marker-line.ts +12 -4
- package/src/migrate-lock-retry.ts +60 -0
- package/src/migrate-rollback.ts +45 -6
- package/src/migrate.ts +45 -4
- package/src/migration-errors.ts +27 -1
- package/src/migration-ledger.ts +38 -4
- package/src/migration-step.ts +50 -22
- package/src/partition-order.ts +56 -0
- package/src/partitioned-tables.ts +24 -0
- package/src/pool-gate.ts +120 -0
- package/src/pool-listeners.ts +143 -0
- package/src/pool-pins.ts +68 -0
- package/src/pool-profile.ts +23 -7
- package/src/pool-reserve.ts +11 -4
- package/src/pool-retire.ts +22 -0
- package/src/pool-sweep.ts +115 -0
- package/src/pool-timer.ts +10 -0
- package/src/pooler-fix.ts +53 -0
- package/src/pooler.ts +161 -0
- package/src/read-transaction.ts +59 -0
- package/src/readonly-query.ts +4 -4
- package/src/replica-client.ts +54 -22
- package/src/replica-position.ts +89 -0
- package/src/replica-scope.ts +25 -4
- package/src/retired.ts +131 -0
- package/src/retype-dependents.ts +2 -0
- package/src/rolling-errors.ts +97 -0
- package/src/rolling.ts +212 -0
- package/src/row-secure-client.ts +54 -0
- package/src/row-security-catalog.ts +26 -0
- package/src/row-security-migrator.ts +48 -0
- package/src/row-security-plan.ts +58 -0
- package/src/row-security-role.ts +60 -0
- package/src/row-security-roles.ts +64 -0
- package/src/row-security.ts +59 -0
- package/src/same-tenant.ts +173 -0
- package/src/schema-dump-policies.ts +26 -0
- package/src/schema-dump-table.ts +21 -6
- package/src/schema-dump.ts +13 -2
- package/src/snapshot-parse.ts +42 -2
- package/src/sqlstate.ts +4 -0
- package/src/statement-bound.ts +82 -0
- package/src/transaction-errors.ts +14 -0
- package/src/transaction-guard.ts +40 -0
- package/src/transaction-settings.ts +24 -0
- package/src/transaction.ts +74 -19
- 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.
|
|
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
|
|
52
|
-
|
|
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** (`
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
|
|
@@ -110,52 +106,29 @@ consults `currentTx()`; `withTransaction` uses `baseClient()`, never `db()`. Kee
|
|
|
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
108
|
`migrate` profile's 3 s). `migration-step.ts` applies a script.
|
|
113
|
-
- **`-- transaction: false` runs a migration outside a transaction** (`concurrent-index.ts
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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`.
|
|
117
113
|
- **The advisory lock is held by one pinned session, and `migrate()`/`rollback()` run every statement
|
|
118
114
|
on it.** `lock: false` reserves nothing and takes no lock; no shipped path passes it
|
|
119
115
|
(`migrate-pin.test.ts`). `migrate.live.test.ts` pins concurrent and failed-midway runs.
|
|
120
|
-
- **One send is one statement**: `applyScript` sends `statementsOf(script)` one at a time.
|
|
121
|
-
**`statement-split.ts` is the only splitter** (a left-to-right scan; `$1` is never a `$tag$`; `\`
|
|
122
|
-
escapes only inside `E''`; comment-only chunks dropped). **`sql-scan.ts` is the one
|
|
123
|
-
lexer** (`noiseAt`, source order; a `$tag$` needs separating from the identifier before it);
|
|
124
|
-
`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`.
|
|
125
117
|
- **`destructive.ts` decides WHAT is destructive**: only `up`, a closed list of four (`drop table`,
|
|
126
118
|
`drop column`, `truncate`, `alter column … type`), decided on blanked text and reported on the
|
|
127
119
|
original; a marker is a top-level line comment (`marker-line.ts`). `X_MIGRATION_DESTRUCTIVE` (ship) and `X_MIGRATION_IRREVERSIBLE` (generate) are
|
|
128
120
|
two questions.
|
|
129
121
|
- **The ledger audit asks one question** — `auditLedger`'s `foreign` filter is `!known.has(row.id)`;
|
|
130
122
|
the app version lives in the cause.
|
|
131
|
-
- **`
|
|
132
|
-
(`rollbackStepsInvalid`, `X_INVARIANT`).
|
|
133
|
-
- **`refuseDependentViews(tx, script)`** (`dependent-view.ts`) runs before each migration's first
|
|
134
|
-
statement: a word scan over `sql-scan.ts` finds retyped columns, one catalog round trip, the pair
|
|
135
|
-
filtered in JS (visible tables only), and `X_MIGRATION_VIEW_DEPENDS` carries the `drop view` /
|
|
136
|
-
`create view` from `pg_get_viewdef` (built through `identifier()` inside a `try`).
|
|
137
|
-
- `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`.
|
|
138
124
|
|
|
139
125
|
## Generation (`x db gen`)
|
|
140
126
|
|
|
141
127
|
- **`generate.ts` reads an index, never re-derives one** — `IndexDescriptionLike` carries columns,
|
|
142
128
|
unique, `where`, `order`, `using`; an index naming no column is `X_INVARIANT`.
|
|
143
|
-
- **An index's ACCESS METHOD is carried end to end
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
`indexMethodSql` re-derives the literal (`X_SQL_UNSAFE` default); a unique or ordered GIN is
|
|
147
|
-
`X_INVARIANT`. `introspectSchema()` reads `pg_am` (`introspect-embedded.test.ts`).
|
|
148
|
-
- **`index-plan.ts` walks both directions** (declared first, removed last); four names are skipped
|
|
149
|
-
(primary, moved aside, rebuilt, over a dropped column). **A removal and a `concurrently: true`
|
|
150
|
-
build on a live table go to the non-transactional step** (`concurrent-plan.ts`; `generate-steps.ts`
|
|
151
|
-
makes it `<id>_concurrently` or the whole migration); a shape a constraint could back
|
|
152
|
-
(`mayBeConstraintBacked`) is unhooked in the step, after its builds. Never recorded (history).
|
|
153
|
-
- **An entity's INVARIANTS reach the DDL** (`invariant-ddl.ts`): a `check` is a named constraint, a
|
|
154
|
-
`unique` a unique INDEX on the one `declaredIndexes` list, an `assert` nothing; `checks` is recorded
|
|
155
|
-
absent-never-`[]`; the name `<table>_<name>_<check|key>` is re-derived, bounded at 63 bytes and
|
|
156
|
-
validated (`constraintNameUnsafe`). **An `assert` is an unrendered loss when a migration recorded its
|
|
157
|
-
CHECK**: `unrenderedOf(entities, current)` takes the recorded schema (required, nullable) and
|
|
158
|
-
`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`.
|
|
159
132
|
- **A COLUMN's CHECK is a named constraint too** (`check-ddl.ts`): one list (`declaredChecks` =
|
|
160
133
|
`columnChecks` then `invariantChecks`), named `<table>_<column>_check` (Postgres' own name); an add on
|
|
161
134
|
an existing column is `drop constraint if exists` then `add constraint`; two declarations naming one
|
|
@@ -166,28 +139,29 @@ consults `currentTx()`; `withTransaction` uses `baseClient()`, never `db()`. Kee
|
|
|
166
139
|
- **`literal()` DOES receive caller input** (`column-default.ts`); `E'…'` only with a backslash, so
|
|
167
140
|
every migration on disk stays byte-identical (`generate-default.live.test.ts`, `sql.test.ts`).
|
|
168
141
|
`readonly-role.ts` and `branch.ts` are safe only by their ordering after `identifier()`.
|
|
169
|
-
- **A retype
|
|
170
|
-
|
|
171
|
-
`sql-scan.ts`); a plain btree survives. `generate-retype.live.test.ts`. What is moved is put back by
|
|
172
|
-
the ordinary diff (`MovedAside`), never twice. **Foreign keys over a retyped column**
|
|
173
|
-
(`retype-keys.ts`): the retype set is derived once for the whole schema (`retypedColumns`), drops go
|
|
174
|
-
in `preAlters` at the top of `up`, re-adding is `foreignKeyPlan`'s, both `breaksOn` ends are needed
|
|
175
|
-
(`generate-retype-key.live.test.ts`). `sql-type.ts` reads `SQL_TYPES` with `Object.hasOwn`.
|
|
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`.
|
|
176
144
|
- **A generated column** (`generated-column.ts`): plain → generated rebuilds (`rebuilt`); the rest of
|
|
177
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`)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
- **
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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,28 +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
|
|
193
|
+
ownership (`pg_depend`, `deptype = 'e'`), views, materialised views, foreign tables, partitions —
|
|
220
194
|
merged into `excluded` unconditionally.
|
|
221
|
-
-
|
|
222
|
-
|
|
223
|
-
- `drift-
|
|
224
|
-
|
|
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`.
|
|
225
204
|
|
|
226
205
|
## The schema dump
|
|
227
206
|
|
|
228
207
|
- **Its own entry: `@ultimat3/db/schema-dump`** (`schema-dump-entry.ts`). The barrel is in every
|
|
229
208
|
role's boot graph and must evaluate none of this family — `schema-dump-entry.test.ts`.
|
|
230
|
-
- **Two readings of one catalog, never mixed.** `introspectSchema()` → `SchemaDescription`, the entity
|
|
231
|
-
vocabulary a snapshot is diffed in. `introspectCatalog()` (`catalog.ts`; queries: `catalog-relations.ts`,
|
|
232
|
-
`catalog-objects.ts`; pure fold: `catalog-fold.ts`) → `CatalogDescription`, Postgres' own `pg_get_*def` text, compared only to
|
|
233
|
-
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`.
|
|
234
210
|
- **Sorted in JS** (`byCodeUnit`), never `order by` and never `localeCompare`: collations differ.
|
|
235
211
|
- **`renderSchemaDump()` is pure**; `schema-dump-table.ts` spells one table. `quoted()` escapes any
|
|
236
212
|
catalog name — `identifier()` refuses whitespace and `"`, which a migration may have created.
|
|
237
213
|
- **`x_` owner → `framework/` twin** (`FRAMEWORK_TABLE_PREFIX`). No list is handed in.
|
|
238
|
-
- **What cannot be rendered is named** (`unrenderedRows
|
|
239
|
-
|
|
240
|
-
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`.
|
|
241
216
|
- **`loadSchemaDump()`**: one transaction, `check_function_bodies` off, a savepoint per file, and
|
|
242
217
|
only `42P01`/`42883`/`42704` are retried. Its refusal is `X_SCHEMA_DUMP_DRIFT` (`dump-drift.ts`).
|
|
243
218
|
- **`object-drift.ts`**: `unexpectedObjects(live, expected)` compares IDENTITY (kind, table, name,
|
|
@@ -245,34 +220,23 @@ consults `currentTx()`; `withTransaction` uses `baseClient()`, never `db()`. Kee
|
|
|
245
220
|
- **No app path here.** Where the dump lives, which engine replays, and when it is written are
|
|
246
221
|
`@ultimat3/cli`'s (`db-schema-dump.ts`). `schema-load.contract.test.ts` reads the reference app's
|
|
247
222
|
migrations as text and runs on Postgres when `TEST_DATABASE_URL` is set.
|
|
248
|
-
- **`pglite-extensions.ts` is the one linker** (`linkPgliteExtensions`
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Only a not-found import is `missing`; a bundle that throws is `X_DB_UNAVAILABLE`.
|
|
252
|
-
- **`pglite-snapshot.ts`**: `snapshotDir` makes a `memory://` boot a restore. Key = PGlite version
|
|
253
|
-
(unreadable = no cache), never the extension set. One file, checksum in its header; unsound or
|
|
254
|
-
unopenable → deleted and rebuilt; unreadable → a miss. Temp name + `rename`. Uncompressed.
|
|
255
|
-
- **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`.
|
|
256
226
|
|
|
257
227
|
## Branches, replicas, read-only
|
|
258
228
|
|
|
259
229
|
- **`reapBranches` sweeps branches of THIS database**: the marker is `ultimate:branch:<base>:<iso>`,
|
|
260
230
|
split on the ISO tail; an older one-segment marker or an unparseable `createdAt` is skipped, never
|
|
261
231
|
dropped. `@ultimat3/cli`'s `ls`/`drop` scope by name prefix.
|
|
262
|
-
- **Read replicas are opt-in twice**: a pool when `DATABASE_REPLICA_URL` names one
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
`@ultimat3/cli`'s boot opens the per-request scope.
|
|
271
|
-
- **This package owns no "is this SQL a write?" lexer** — `readonly.ts` and `X_READONLY_VIOLATION`
|
|
272
|
-
are deleted; `errors.test.ts` pins `DB_OWNED_ERROR_CODES`. The layers are `ensureReadOnlyRole()`
|
|
273
|
-
(layer 1, returns `null` on a missing permission) and `readOnlyQuery()` (layer 2: `BEGIN READ ONLY`
|
|
274
|
-
+ statement timeout; it THROWS), with `@ultimat3/mcp` as layers 3–4. **`readOnlyQuery` takes ONE
|
|
275
|
-
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`.
|
|
276
240
|
|
|
277
241
|
```bash
|
|
278
242
|
bun test # from packages/db
|