@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.
- package/CLAUDE.md +78 -116
- package/README.md +199 -16
- 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 +157 -0
- package/src/concurrent-plan.ts +294 -0
- 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 +21 -35
- 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 +75 -0
- package/src/errors.ts +80 -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 +99 -0
- package/src/generate-triggers.ts +119 -0
- package/src/generate-view.ts +97 -0
- package/src/generate.ts +128 -89
- package/src/index-ddl.ts +23 -35
- package/src/index-plan.ts +35 -14
- package/src/index.ts +136 -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 +47 -0
- package/src/migrate-lock-retry.ts +60 -0
- package/src/migrate-rollback.ts +45 -6
- package/src/migrate.ts +67 -69
- package/src/migration-errors.ts +48 -1
- package/src/migration-ledger.ts +38 -4
- package/src/migration-step.ts +192 -0
- 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
|
|
|
@@ -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
|
|
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;
|
|
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
|
-
- **`
|
|
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
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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`)
|
|
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,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
|
|
193
|
+
ownership (`pg_depend`, `deptype = 'e'`), views, materialised views, foreign tables, partitions —
|
|
220
194
|
merged into `excluded` unconditionally.
|
|
221
|
-
-
|
|
222
|
-
|
|
223
|
-
- **`dbDrift()` lives in `drift-errors.ts`** (it needs `shellInertIdentifier`)
|
|
224
|
-
|
|
225
|
-
-
|
|
226
|
-
|
|
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
|
|
241
|
-
|
|
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`
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|