@voltro/database 0.77.1 → 0.79.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/CHANGELOG.md +96 -0
- package/dist/index.d.ts +124 -16
- package/dist/index.js +528 -528
- package/dist/{replaceInProgress-DdjHyYFT.js → replaceInProgress-BmSjNTp0.js} +467 -458
- package/dist/sql.d.ts +435 -14
- package/dist/sql.js +1665 -1293
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,102 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.79.0] — 2026-09-23
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **Probes answer on the app port from the first moment, on every command** — `@voltro/cli`, `@voltro/runtime`, `@voltro/voltro`
|
|
47
|
+
|
|
48
|
+
`voltro dev`, `voltro serve` and `voltro start` now bind their app port as the FIRST thing they do once they know it, and answer `/internal/liveness` (200), `/internal/readiness` (503 until the app serves, then 200 with the dependency ping) and `/internal/startup` (JSON `{ phase, ready, elapsedMs, … }`, 503 → 200) there through the whole boot — a slow database connect, a fingerprint check or a cold migrate included. Every other request and every WebSocket upgrade gets `503 + Retry-After: 1` until ready, then the app serves on the same socket and the probe paths keep answering before routing. There is ONE server and ONE port, so a probe path means the same thing on every command; `voltro dev` no longer binds a second probe server on app port + 1 and no longer reads `VOLTRO_DEV_HEALTH_PORT`. Migration: point every probe of a pod running `voltro dev` at the app port and delete the variable — the codemod prints the exact steps and finds the variable in compose, Helm and CI files.
|
|
49
|
+
|
|
50
|
+
**`voltro update` carries you across this** — codemod `0.79.0/01_probes-on-the-app-port`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.79.0).
|
|
51
|
+
- **A rejected credential is refused on the default tenant policy too** — `@voltro/cli`, `@voltro/protocol`, `@voltro/voltro`
|
|
52
|
+
|
|
53
|
+
A request whose credential a strategy rejects (an expired or malformed token, a revoked session or key) now ends with `Unauthenticated` (`credential rejected: <reason>`) on every anonymous-tenant policy. Until now that held under `auth: { anonymousTenantId }` and `auth: { anonymousTenantRequired: true }`, but not for an app that sets neither. There, a procedure without a guard served the caller as an anonymous one, and an open query answered public data to a token that had just been refused. A caller who sends no credential is unaffected: it still resolves to the tenant its `x-tenant` header names, or to the development tenant. `composeAuthStrategies` gains `anonymousTenantDefault` for that default tenant. Unlike a `fallback`, it does not take over the rejected-credential decision. A client that kept sending a stale cookie or token and relied on the anonymous answer now receives `Unauthenticated`, which the error bus reports and `wireAuthRefresh` acts on.
|
|
54
|
+
|
|
55
|
+
**`voltro update` carries you across this** — codemod `0.79.0/03_rejected-credentials-refused-on-the-default-policy`. If you pin versions by hand and never run it, print the notes without changing anything: `voltro update --codemods-only --from <your current version> --dry-run` (this one ships in 0.79.0).
|
|
56
|
+
|
|
57
|
+
### Added
|
|
58
|
+
|
|
59
|
+
- **A 1366 over a 4-byte character says which config field fixes it** — `@voltro/database`
|
|
60
|
+
|
|
61
|
+
When MySQL or MariaDB refuse a write with `Incorrect string value: '\xF0…'` (errno 1366 over a four-byte UTF-8 lead byte — an emoji into a `utf8mb3` column), the store's `detail:` line now carries a `hint:` naming the column and the remedy: declare `store: { dialect, charset: 'utf8mb4' }` in `app.config.ts` and run `voltro db plan`. A 1366 for any other reason gets no advice about character sets.
|
|
62
|
+
- **The store's character set is declared once and every table follows it** — `@voltro/database`, `@voltro/cli`, `@voltro/devtools-ui`
|
|
63
|
+
|
|
64
|
+
On MySQL and MariaDB the framework's `CREATE TABLE` named no character set, so a `text()` column inherited whatever the database was created with while a `json()` column did not (MariaDB's `JSON` is `LONGTEXT CHARACTER SET utf8mb4`, MySQL's has none) — and on a `utf8mb3` database one table refused an emoji in its text column with errno 1366 and took it in its json column. `app.config.ts` now takes `store: { dialect: 'mariadb' | 'mysql', charset: 'utf8mb4' | 'utf8mb3' }`: every declared table is created with it as its default and, when a live table's string columns are stored otherwise, converted to it by a new `convert-charset` plan operation — a table rewrite that drops and re-creates the foreign keys touching the table, so a pair bound by a constraint converts together instead of one side failing with errno 150. Widening to `utf8mb4` is `safe`; narrowing to `utf8mb3` is `lossy` and waits for the destructive opt-in. json columns keep the engine's own charset (neither engine lets it be declared). Unset, nothing changes. A charset on any other dialect is refused at boot, and a `DB_DIALECT` override off the mysql family drops it with a logged line rather than silently.
|
|
65
|
+
|
|
66
|
+
The public signatures that changed only gain optional trailing parameters (`declaredSnapshot`, `emitNamespacedSchemaSql`, `emitNamespaceProvisionDdl`, the schema-emit options) and one union member (`convert-charset` in the migration operation kind), so existing calls compile unchanged. Only an exhaustive `switch` over that operation kind would need a new case.
|
|
67
|
+
- **The plan and the doctor name tables whose text cannot hold what json can** — `@voltro/cli`, `@voltro/database`
|
|
68
|
+
|
|
69
|
+
Without a declared store charset, `voltro db plan` prints — under the plan — the MySQL / MariaDB tables whose text columns are stored in a character set narrower than their json columns' `utf8mb4`, and the tables whose string columns are not all in one character set (a column converted by hand). `voltro doctor` has the same section, reading `information_schema`, with three outcomes that never print alike: not applicable off the mysql family, not checked with the reason when no database is reachable, and the findings — against the declaration when one is set. A healthy `utf8mb4` database reports nothing; in `--json` the section is `storeCharset`.
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- **A handler's own `Unauthenticated` reaches the client typed, on any procedure** — `@voltro/protocol`, `@voltro/cli`, `@voltro/runtime`
|
|
74
|
+
|
|
75
|
+
`Unauthenticated` and `ScopeError` are now members of every procedure's wire error union — queries, mutations, actions, streams and workflow starts, whether or not the descriptor declares a guard — and `Unauthenticated` of every event's. They used to ride only a procedure with an enforced `guards:` entry, on the reasoning that an open procedure cannot produce them. It can: the auth middleware refuses an open procedure with a rejected credential exactly as it refuses a guarded one, and a handler may refuse on its own with `assertAuthenticated` or `requireScope`. On a procedure without a guard the server judged that handler-side refusal undeclared and collapsed it, so the browser received `{ _tag: 'InternalError', message: 'Unauthenticated: …' }` — the typed error's name inside a defect that no `_tag` match and no credential-refresh policy could see — and the generated client had no schema to decode the middleware's own refusal with. Both ends now carry the same union, so the value arrives as `{ _tag: 'Unauthenticated' }` on the failure channel over `POST /rpc` and over the WebSocket, and a gRPC surface maps it to `UNAUTHENTICATED` rather than `FAILED_PRECONDITION`. Measured under `voltro dev` and `voltro serve` with a validly signed, expired JWT driven through the real client over the socket.
|
|
76
|
+
- **`voltro build` walks an api app's module closure once, not once per handler file** — `@voltro/cli`
|
|
77
|
+
|
|
78
|
+
Each api artefact (`apiEntry.js`, the serve bundle) is preceded by a closure-discovery pass that only reads esbuild's metafile. It used to hand esbuild every discovery file as its own entry point, and without code splitting esbuild links every entry point separately, each on its own copy of the module graph, all at once. On a synthetic api app of 6,747 modules (4,514 handler files) that probe alone peaked at 4.3 GiB in the esbuild service — ten times the bundle it precedes — and it ran twice per build, so the whole build peaked at 5–6 GiB and could be refused by a shared host with less free memory than that. The probe now links one generated entry that imports every discovery file; the closure it finds is the same set. Measured on the same app the build's peak is under 2 GiB (esbuild 1.9 GiB, Node 0.5 GiB), at 3,048 modules 1.0 GiB, at 1,047 modules 0.9 GiB. The build page documents the expectation.
|
|
79
|
+
- **The file-migration `phase` rewrite reaches aliased, held and nested specs** — `@voltro/cli`
|
|
80
|
+
|
|
81
|
+
For a project crossing 0.78.0, the `0.78.0/01_file-migration-phase` rewrite now also finds `migration` imported under another name, a spec held in a local `const`, and migrations in subdirectories of `migrations/`. All three are shapes the runner discovers, and the rewrite used to skip them without a word. A spec built outside the call (by a helper, or imported) is not rewritten, because the change would land in code other callers share. Instead the rewrite names it by file and line. `migration()` still refuses a spec without a phase when the file loads.
|
|
82
|
+
- **A subscription refused in-band reaches the error bus and the refresh policy** — `@voltro/client`
|
|
83
|
+
|
|
84
|
+
A guard's refusal on a subscription travels in-band — a `{ _tag: 'error' }` event on that one stream, so a sibling subscription on the shared socket is not stalled. The subscription cache set the snapshot's error from it and told nobody else: the api's error bus, which `wireAuthRefresh` and `useOnRpcError` listen on, heard only streams that failed outright. So an expired credential on a read never asked the host for a fresh token and never reached an error reporter. The in-band value is now emitted to the bus with `source: 'subscription'` and `kind: 'handler'`, the same event a failed stream produces.
|
|
85
|
+
- **`voltro support collect` no longer fails on a temporary directory under src/** — `@voltro/cli`
|
|
86
|
+
|
|
87
|
+
The conflict-key inventory in `voltro support collect` found its files with a glob that skips dot-directories, then loaded them through a call that walked the directories again. So a temporary directory that an editor, a test run or a build tool creates and removes under `src/` could fail the whole collection with `Directory not found`, and an unreadable dot-directory failed it with `EACCES`. Only the files the glob returned are loaded now. A file removed between the two steps is skipped.
|
|
88
|
+
- **Tenant namespaces on MySQL / MariaDB provision without USE and take the charset** — `@voltro/database`, `@voltro/cli`
|
|
89
|
+
|
|
90
|
+
Provisioning a tenant namespace on the mysql family emitted `USE <database>` followed by unqualified DDL. MySQL 8.4 refuses `USE` in the prepared protocol (errno 1295), so under `tenancy.isolation: 'namespace'` no tenant could be provisioned on MySQL at all; on MariaDB the `USE` outlived its transaction on the pooled connection, which then served its next unqualified statement in the tenant database. The tenant DDL is now database-qualified, as the store's namespace view already was. The declared store charset reaches the tenant too: a new tenant database is created with it as its default and its tables carry it. A tenant provisioned before the declaration is named per namespace by `voltro db plan` and `voltro doctor` rather than silently left as it was.
|
|
91
|
+
- **`voltro update` moves the `@voltro/*` pins in `pnpm-workspace.yaml` too** — `@voltro/cli`
|
|
92
|
+
|
|
93
|
+
`voltro update` used to bump only the package.json files. A pnpm workspace that also names framework versions in `pnpm-workspace.yaml` was left behind in two ways. `overrides:` entries held the tree on the old release. And `minimumReleaseAgeExclude:` entries (`'@voltro/<pkg>@<version>'`, which admit a release younger than pnpm's release-age window) went stale, so pnpm refused the new release for that whole window. Both blocks now move with the bump. An entry moves only when it names exactly the version being left; an entry at any other version is printed as `kept`. A block written as a flow list is named rather than rewritten. The packages named there count for `--wait`, and the file is restored along with the package.json files when an install fails before anything lands.
|
|
94
|
+
- **`voltro update --wait` waits up to 45 minutes, longer than a release holds** — `@voltro/cli`
|
|
95
|
+
|
|
96
|
+
`voltro update --wait` polls the registry until every package it bumps is published at the target version, `@voltro/dashboard` included. It used to give up after 20 minutes. A release, though, holds `latest` for up to 30 minutes after its other packages finish uploading while `@voltro/dashboard` is still being built. So an update that started polling at the first upload could fail after a release that went on to complete normally. The ceiling is now 45 minutes, which covers the release's hold plus its upload phase. The update still reports the late packages while it waits, and on timeout it still restores the bumps byte for byte.
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## [0.78.0] — 2026-09-22
|
|
101
|
+
|
|
102
|
+
### ⚠ BREAKING
|
|
103
|
+
|
|
104
|
+
- **A file migration declares whether it runs before or after the schema apply** — `@voltro/database`, `@voltro/cli`
|
|
105
|
+
|
|
106
|
+
`migration({ … })` now requires `phase: 'before-schema' | 'after-schema'`. `'before-schema'` is the order every file migration has always run in — first, before the planner diffs declared against live — and is unchanged. The new `'after-schema'` phase runs LAST in the same job, once the plan has been applied and the plugin steps and change triggers have converged, which is where a body that needs the new shape belongs: a back-fill into a table the same deploy creates could previously only meet its own `skipUnless` guard, be recorded, and never run again. Both phases run under the same failure gate in `voltro migrate`, `voltro db apply`, `voltro db files` and the `voltro dev` auto-migrate; `voltro db apply --plan` refuses a pending `before-schema` migration as before and applies a pending `after-schema` one. Migration: run `voltro update` (the codemod adds `phase: 'before-schema'` to every existing migration), or add the field by hand.
|
|
107
|
+
|
|
108
|
+
### Added
|
|
109
|
+
|
|
110
|
+
- **A migrate run names a skip the same run makes permanent** — `@voltro/database`, `@voltro/cli`
|
|
111
|
+
|
|
112
|
+
A `before-schema` file migration that stops at `ctx.skipUnless` because a schema object is absent is recorded as applied and never offered again — so when the plan computed immediately afterwards CREATES that object, the skip could never have been anything else. `voltro db apply`, `voltro migrate` and the `voltro dev` auto-migrate now say so, naming the objects the body found missing that the plan goes on to create and the phase that would have run it. The objects are measured from the `ctx.schema` existence questions the body actually asked, not read out of the skip reason, so a body that skips for a reason unrelated to the schema is not reported. Nothing is refused: by the time both facts exist the migration is already recorded.
|
|
113
|
+
|
|
114
|
+
### Fixed
|
|
115
|
+
|
|
116
|
+
- **`latest` waits for the whole release, the dashboard included** — `@voltro/cli`
|
|
117
|
+
|
|
118
|
+
Every `@voltro/*` package of a release carries the same version, and `@voltro/dashboard` is published from its own repository once the rest are on the registry — so for the length of that run the release existed for every package but one, while `latest` already named it. Measured across the three releases before this change: the dashboard landed 8m43s, 9m00s and 8m17s after `@voltro/cli`. An installation started in that window against the newest published version bumps every pin and then fails with `ERR_PNPM_NO_MATCHING_VERSION` on the one package that does not have it yet. The release now holds the `latest` tag until every package of the lockstep set is downloadable — the dashboard is waited for exactly like a tarball that has not reached the CDN — so the newest published version is one that can be installed whole. Until it is complete, `latest` stays on the previous release rather than naming a partial one. An update that names an explicit version can still start mid-release, because a version resolves the moment its own package uploads; `voltro update --wait` remains the way to cover that.
|
|
119
|
+
- **A migration body guarding on `schema.tableExists` runs in a database-free test** — `@voltro/database`, `@voltro/runtime`
|
|
120
|
+
|
|
121
|
+
A file migration whose first statement is the guard the docs recommend — `ctx.skipUnless(ctx.schema.tableExists('…'), '…')` — could not be driven over the in-memory store: `skipUnless` worked without a SQL client, but its predicate was a `ctx.schema` call and every one of those refused, so the only way to test such a body was to split the guard out of `up`. A migration context built without a client now answers the two read-only probes the store can answer honestly about itself — `schema.tableExists` from the store's own table map, `schema.columnExists` from the keys the held rows carry — through the new optional `DataStore.storedTables()` capability, which `InMemoryDataStore` and its transaction view implement. A table that is present and empty exists; `columnExists` on an empty table refuses rather than answering `false`, because it holds no evidence either way. `schema.snapshot`, `schema.indexExists`, `schema.foreignKeyExists` and every schema change still refuse — a store has no catalog, and a fabricated answer is one a migration would branch on — and each refusal now names what to do instead.
|
|
122
|
+
|
|
123
|
+
Two more fixes on the same surface, both found by running a PROMISE-form body through the guard rather than only an Effect one. `skipUnless` reached the runner wrapped in a `FiberFailure`, so the migration FAILED instead of being recorded as skipped. And a `ctx.schema` call from a Promise body inside a transactional migration asked the pool for a connection instead of using the transaction's, which on a single-connection dialect is the connection the transaction is holding — the migration hung forever. Every `ctx.schema` member now executes on the store's own connection, so both body shapes reach the same outcome on every dialect.
|
|
124
|
+
- **A storage change no longer reads as `text → text` in a migration plan** — `@voltro/cli`, `@voltro/database`
|
|
125
|
+
|
|
126
|
+
A change to a column's storage PARAMETERS — a text length, a decimal precision — keeps the column's type, so the planned operation carries the same type on both sides. Two readers rendered that as the type change it is not: the plan row showed `tier text → text`, which is indistinguishable from a no-op, and the rolling-deploy advisory said "the new text type can reject their writes", which names nothing to act on. Both now say the storage is what moves, and the advisory's remedy is the one that applies — widen now, narrow in a later deploy once every writer produces values that fit. The numbers were never lost: the operation's reason carries them (`text length 65535 → 16`, with the count query for a narrowing) and still does. Reachable for an ordinary edit: adding `.oneOf([...])` to an existing unbounded `text()` column renders a bounded column on every dialect that can express a width, measured on MySQL and MariaDB.
|
|
127
|
+
- **TypeScript 7 removed thirteen tsconfig options, not the three we named** — `@voltro/cli`
|
|
128
|
+
|
|
129
|
+
The TypeScript 7 upgrade note named three removed tsconfig options. A workspace that followed it hit a fourth — `"esModuleInterop": false`, in twelve of its configs, where the ones that set `true` typechecked cleanly so only part of the workspace failed. Adding a fourth bullet would have left nine more waiting, so the set was measured instead: every boolean option TypeScript 6.0.3 accepts, written into a tsconfig in both spellings, plus every value of every enum option, typechecked against TypeScript 7.0.2. Thirteen are refused, in two shapes — an option whose value alone is gone (`esModuleInterop=false`, `allowSyntheticDefaultImports=false`, `alwaysStrict=false`, `target=ES5`, `module=AMD`/`System`/`UMD`, `moduleResolution=node10`/`Classic`) and one removed outright (`downlevelIteration`, `outFile`, `baseUrl`). A new note carries all of them with their replacements and three `git grep` lines that find every occurrence at once, and it fires only for a config that actually carries one — the accepted spelling stays quiet. It also says the thing the codes cannot: TS5108 covers several unrelated removals, so what identifies a case is the option and value in the message, not the code. The framework's own templates carry none of the thirteen; checked, not assumed.
|
|
130
|
+
|
|
131
|
+
The gate matches a PATTERN rather than a literal, which is new for a codemod that reads config: JSON permits whitespace wherever a token boundary allows, so `"esModuleInterop" : false` and a literal list of spellings disagree — and a spelling the list forgets is silence from the gate, which reads as nothing to do.
|
|
132
|
+
- **Workflow submission rows carry one identity shape again** — `@voltro/cli`, `@voltro/workflow`
|
|
133
|
+
|
|
134
|
+
Starts of a workflow with a declared concurrency limit wrote their internal submission row with an identity prefix one character shorter than the one every other start path writes, so `_voltro_workflow_submissions` ended up holding an identity shape nothing declared. That is not cosmetic: pool release recovery pages this table by id under the database's own collation, and the two shapes sort in one order there and the opposite order in the serving process — measured on MariaDB with `utf8mb3_general_ci`, where a census over a mixed page stopped at the row that crosses the shapes and left every pooled permit held. All start paths now mint a new identity through one function. The one other shape this table holds is deliberate — a retry request reuses its own content-addressed identity so a retried reservation finds its own row — and recovery tolerates it. Rows written by earlier versions keep their identity and are read normally; nothing needs to be migrated.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
42
138
|
## [0.77.1] — 2026-09-22
|
|
43
139
|
|
|
44
140
|
### Fixed
|
package/dist/index.d.ts
CHANGED
|
@@ -2035,6 +2035,25 @@ declare interface ColumnSnapshot {
|
|
|
2035
2035
|
* blocks a masking export until classified.
|
|
2036
2036
|
*/
|
|
2037
2037
|
readonly safe?: boolean;
|
|
2038
|
+
/**
|
|
2039
|
+
* The character set a STRING column is stored in — MySQL / MariaDB only, and
|
|
2040
|
+
* only on columns the engine gives one (text, varchar, enum; never json,
|
|
2041
|
+
* which is `utf8mb4` on MariaDB and charset-less on MySQL by construction,
|
|
2042
|
+
* and never a number, a date or bytes).
|
|
2043
|
+
*
|
|
2044
|
+
* Live side: what `information_schema.columns.character_set_name` reports.
|
|
2045
|
+
* Declared side: the store's declared charset (`store: { dialect, charset }`
|
|
2046
|
+
* in `app.config.ts`), stamped on every string column — ABSENT when the app
|
|
2047
|
+
* declares none. The planner compares it only when BOTH sides carry one, so
|
|
2048
|
+
* an undeclared app never plans a conversion; and the fingerprint never
|
|
2049
|
+
* hashes it, because a live reading against an absent declaration would part
|
|
2050
|
+
* every table's two hashes forever. `storeCharset.ts` carries the reason.
|
|
2051
|
+
*
|
|
2052
|
+
* A `string`, not `MysqlCharset`: the reader reports whatever the column is
|
|
2053
|
+
* in (`latin1` is real), and a conversion FROM it is exactly what the
|
|
2054
|
+
* declaration is for. Only the declared side is limited to the two.
|
|
2055
|
+
*/
|
|
2056
|
+
readonly charset?: string;
|
|
2038
2057
|
/**
|
|
2039
2058
|
* Type PARAMETERS for the three `ColumnType`s whose DDL is not determined by
|
|
2040
2059
|
* the type tag alone — `vector(n)`, `array(of)`, `enum(name, values)`.
|
|
@@ -2961,6 +2980,42 @@ export declare interface DataStore {
|
|
|
2961
2980
|
* a way to hand a store somebody else's connection.
|
|
2962
2981
|
*/
|
|
2963
2982
|
run?<A, E>(effect: Effect.Effect<A, E, SqlClient.SqlClient>): Promise<A>;
|
|
2983
|
+
/**
|
|
2984
|
+
* What this store can say about its OWN shape without a catalog — the
|
|
2985
|
+
* tables it holds and the columns their rows carry.
|
|
2986
|
+
*
|
|
2987
|
+
* The inverse capability of {@link DataStore.run}. A SQL store answers a
|
|
2988
|
+
* schema question by asking the server's catalog, which is authoritative
|
|
2989
|
+
* about far more than this (types, indexes, constraints) and is therefore
|
|
2990
|
+
* the right source whenever there is a connection. A store that IS its data
|
|
2991
|
+
* has no catalog — and is not thereby ignorant: it knows exactly which
|
|
2992
|
+
* tables it holds, because holding them is all a table is there. That much
|
|
2993
|
+
* is answerable honestly, and nothing beyond it is.
|
|
2994
|
+
*
|
|
2995
|
+
* So the contract is deliberately narrow, and the narrowness is the point:
|
|
2996
|
+
*
|
|
2997
|
+
* - `table` is unqualified by whatever namespace the view is bound to, so
|
|
2998
|
+
* a tenant view reports the tenant's tables under their bare names.
|
|
2999
|
+
* - `columns` is the UNION of the keys the held rows carry. An empty
|
|
3000
|
+
* table therefore reports NO columns, and that is not a claim that it
|
|
3001
|
+
* has none — a caller must read `rowCount` before treating an absent
|
|
3002
|
+
* column as absent.
|
|
3003
|
+
* - `rowCount` is how many rows are held, so "the table is empty and says
|
|
3004
|
+
* nothing about its columns" is distinguishable from "the table has
|
|
3005
|
+
* rows and none of them carries that column".
|
|
3006
|
+
*
|
|
3007
|
+
* OPTIONAL on the interface: the in-memory store implements it (it is the
|
|
3008
|
+
* only store whose data IS its schema); SQL stores omit it, because the
|
|
3009
|
+
* catalog answers the same questions better and a second, weaker source of
|
|
3010
|
+
* truth beside it is how the two drift. Callers guard with
|
|
3011
|
+
* `if (store.storedTables)`.
|
|
3012
|
+
*
|
|
3013
|
+
* Read by the file-migration context built WITHOUT a `sql` client
|
|
3014
|
+
* (`makeFileMigrationContext`), so a migration body that opens with
|
|
3015
|
+
* `ctx.skipUnless(ctx.schema.tableExists('…'), …)` — the guard the file
|
|
3016
|
+
* migration docs ask for — runs unchanged in a database-free test.
|
|
3017
|
+
*/
|
|
3018
|
+
storedTables?(): ReadonlyArray<StoredTableShape>;
|
|
2964
3019
|
}
|
|
2965
3020
|
|
|
2966
3021
|
/** One transfer, as the inspector reads it back. */
|
|
@@ -3317,20 +3372,19 @@ export declare const deriveTypeIdPrefix: (tableName: string) => string;
|
|
|
3317
3372
|
export declare const describeAgedSoftDropSnapshots: (aged: ReadonlyArray<AgedSoftDropSnapshot>, retentionDays: number) => string;
|
|
3318
3373
|
|
|
3319
3374
|
/**
|
|
3320
|
-
*
|
|
3321
|
-
*
|
|
3322
|
-
*
|
|
3323
|
-
*
|
|
3324
|
-
*
|
|
3325
|
-
*
|
|
3326
|
-
* four different fixes and one string. The driver's own words are in the cause,
|
|
3327
|
-
* and the cause hangs off a SYMBOL on a `FiberFailure`, so a caller who reaches
|
|
3328
|
-
* for `.cause` finds `undefined` and concludes there is nothing there.
|
|
3375
|
+
* The one 1366 a schema can explain: a 4-byte character (an emoji, most
|
|
3376
|
+
* supplementary-plane text) written to a MySQL / MariaDB column stored in
|
|
3377
|
+
* `utf8mb3`. The driver's sentence names the bytes and the column — `Incorrect
|
|
3378
|
+
* string value: '\xF0\x9F\xA7\xA9…' for column `description`` — and stops
|
|
3379
|
+
* there, which read as a data problem to a deployment that had the answer one
|
|
3380
|
+
* config field away.
|
|
3329
3381
|
*
|
|
3330
|
-
*
|
|
3331
|
-
*
|
|
3332
|
-
*
|
|
3382
|
+
* Only for a lead byte `F0`–`F4` (a four-byte UTF-8 sequence). A 1366 over a
|
|
3383
|
+
* malformed sequence or a wrong-type value is a different problem and gets no
|
|
3384
|
+
* advice about character sets. Names the real config field and nothing else.
|
|
3333
3385
|
*/
|
|
3386
|
+
export declare const describeCharsetRefusal: (dbCause: Record<string, unknown>) => string | undefined;
|
|
3387
|
+
|
|
3334
3388
|
export declare const describeDriverError: (err: unknown) => string | undefined;
|
|
3335
3389
|
|
|
3336
3390
|
/**
|
|
@@ -3966,6 +4020,31 @@ export declare interface FileMigration {
|
|
|
3966
4020
|
* schema change through `ctx.schema` in a migration of its own).
|
|
3967
4021
|
*/
|
|
3968
4022
|
readonly transactional?: boolean;
|
|
4023
|
+
/**
|
|
4024
|
+
* WHERE in a migrate run this body belongs, relative to the declarative
|
|
4025
|
+
* schema apply. Required, and there is no default, because the answer cannot
|
|
4026
|
+
* be inferred from the body and getting it wrong is silent.
|
|
4027
|
+
*
|
|
4028
|
+
* - **`'before-schema'`** — runs FIRST, before the planner diffs declared
|
|
4029
|
+
* against live. This is the phase for everything a state diff cannot infer
|
|
4030
|
+
* and must not be computed against: a table split, a merge, a foreign key
|
|
4031
|
+
* dropped so the planner does not trip over it, a type change with a USING
|
|
4032
|
+
* expression. The plan is computed AFTER this body, against the shape it
|
|
4033
|
+
* left behind.
|
|
4034
|
+
* - **`'after-schema'`** — runs LAST, after the plan has been applied and the
|
|
4035
|
+
* plugin steps and change triggers have converged. This is the phase for a
|
|
4036
|
+
* body that NEEDS the new shape: a back-fill into a table the same deploy
|
|
4037
|
+
* creates, a column filled from its neighbours. It runs in the same job,
|
|
4038
|
+
* under the same failure gate, and — on a dialect that holds a deploy in
|
|
4039
|
+
* one transaction — inside the same unit as the schema it depends on.
|
|
4040
|
+
*
|
|
4041
|
+
* A back-fill declared `'before-schema'` is the mistake this field exists to
|
|
4042
|
+
* make unavailable: on the deploy that creates its table the body can only
|
|
4043
|
+
* take its own `skipUnless` guard, and the skip is recorded, so it never gets
|
|
4044
|
+
* a second chance. The runner names that case when it can see it, but a
|
|
4045
|
+
* warning after the fact is the weaker half — the phase is the answer.
|
|
4046
|
+
*/
|
|
4047
|
+
readonly phase: MigrationPhase;
|
|
3969
4048
|
/** The forward body. */
|
|
3970
4049
|
readonly up: (ctx: FileMigrationContext) => Promise<void> | Effect.Effect<void, unknown, never>;
|
|
3971
4050
|
/**
|
|
@@ -5128,6 +5207,7 @@ export declare type MergeMixinFields<Mixins extends ReadonlyArray<AnyMixin>> = M
|
|
|
5128
5207
|
* export default migration({
|
|
5129
5208
|
* id: '20260415_120000_split_address_out',
|
|
5130
5209
|
* description: 'Move users.address* into a separate addresses table.',
|
|
5210
|
+
* phase: 'before-schema',
|
|
5131
5211
|
* up: ({ sql, store, log }) => Effect.gen(function* () {
|
|
5132
5212
|
* yield* sql.unsafe(`CREATE TABLE addresses ( ... )`)
|
|
5133
5213
|
* yield* sql.unsafe(`INSERT INTO addresses (...) SELECT (...) FROM users`)
|
|
@@ -5139,6 +5219,9 @@ export declare type MergeMixinFields<Mixins extends ReadonlyArray<AnyMixin>> = M
|
|
|
5139
5219
|
*/
|
|
5140
5220
|
export declare const migration: (spec: FileMigration) => FileMigration;
|
|
5141
5221
|
|
|
5222
|
+
/** Both phases, in the order a run executes them. The one definition; readers derive. */
|
|
5223
|
+
export declare const MIGRATION_PHASES: readonly [MigrationPhase, MigrationPhase];
|
|
5224
|
+
|
|
5142
5225
|
export declare interface MigrationDefinition {
|
|
5143
5226
|
readonly __migration: true;
|
|
5144
5227
|
readonly id: string;
|
|
@@ -5182,10 +5265,13 @@ export declare class MigrationExpectationFailed extends Error {
|
|
|
5182
5265
|
/**
|
|
5183
5266
|
* A migration context was built without a SQL client — over the in-memory
|
|
5184
5267
|
* store, the shape a unit test uses — and the body reached a surface that
|
|
5185
|
-
* needs a live connection: `raw()`,
|
|
5186
|
-
*
|
|
5187
|
-
*
|
|
5188
|
-
*
|
|
5268
|
+
* needs a live connection: `raw()`, an Effect body under `ctx.transactional`
|
|
5269
|
+
* (which runs ON the connection), or a `ctx.schema` member that can only be
|
|
5270
|
+
* answered from a CATALOG — `snapshot`, `indexExists`, `foreignKeyExists` and
|
|
5271
|
+
* every schema CHANGE. `each`, `rewrite`, `expect`, `skipUnless`, `store`,
|
|
5272
|
+
* `log` and the two probes the store can answer about its own shape
|
|
5273
|
+
* (`tableExists`, `columnExists`) work without one; each refusal names what to
|
|
5274
|
+
* do instead. Also raised at construction when what was handed in as `sql` is not
|
|
5189
5275
|
* an `@effect/sql` client, or when a SQL-backed store arrives without the
|
|
5190
5276
|
* client it executes on — both are refused before a body can run rather
|
|
5191
5277
|
* than surfacing as a TypeError from inside the first helper that touches
|
|
@@ -5198,6 +5284,18 @@ export declare class MigrationNeedsSqlClient extends Error {
|
|
|
5198
5284
|
constructor(migrationId: string, operation: string, detail: string);
|
|
5199
5285
|
}
|
|
5200
5286
|
|
|
5287
|
+
/**
|
|
5288
|
+
* The two points in a migrate run at which a file-based body can execute.
|
|
5289
|
+
*
|
|
5290
|
+
* The ORDER of `'before-schema'` is what every file migration has always had
|
|
5291
|
+
* and is not negotiable: a body that drops a constraint the planner would trip
|
|
5292
|
+
* over has to run before the plan is computed, and `voltro db apply --plan`
|
|
5293
|
+
* refuses a pending one for exactly that reason. `'after-schema'` is the point
|
|
5294
|
+
* that did not exist — everything the declarative apply produces is in place,
|
|
5295
|
+
* so a body may depend on it.
|
|
5296
|
+
*/
|
|
5297
|
+
export declare type MigrationPhase = 'before-schema' | 'after-schema';
|
|
5298
|
+
|
|
5201
5299
|
/**
|
|
5202
5300
|
* Schema questions and schema operations a migration body can ask of the
|
|
5203
5301
|
* database it is running against, spelled once for every dialect.
|
|
@@ -7943,6 +8041,16 @@ export declare const stampGeneratedIds: <T extends Record<string, unknown>>(tabl
|
|
|
7943
8041
|
*/
|
|
7944
8042
|
export declare const startsWith: <RowOf = Record<string, unknown>, K extends keyof RowOf & string = keyof RowOf & string>(column: K, prefix: string) => PredicateLeaf;
|
|
7945
8043
|
|
|
8044
|
+
/** One table as a store that has no catalog can describe it. See {@link DataStore.storedTables}. */
|
|
8045
|
+
export declare interface StoredTableShape {
|
|
8046
|
+
/** The table's name, unqualified by any namespace the reporting view is bound to. */
|
|
8047
|
+
readonly table: string;
|
|
8048
|
+
/** The union of the column names the held rows carry. Empty for a table with no rows. */
|
|
8049
|
+
readonly columns: ReadonlyArray<string>;
|
|
8050
|
+
/** How many rows the store holds for this table. `0` means `columns` carries no evidence. */
|
|
8051
|
+
readonly rowCount: number;
|
|
8052
|
+
}
|
|
8053
|
+
|
|
7946
8054
|
/** The facts a framework refusal carries into `ConstraintViolation`. */
|
|
7947
8055
|
export declare interface StoreRefusal {
|
|
7948
8056
|
/** Which integrity rule the caller ran into. Mirrors
|