@voltro/database 0.77.1 → 0.78.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 +38 -0
- package/dist/index.d.ts +94 -4
- package/dist/index.js +642 -642
- package/dist/{replaceInProgress-DdjHyYFT.js → replaceInProgress-B1ZgTEI5.js} +445 -444
- package/dist/sql.d.ts +199 -9
- package/dist/sql.js +1391 -1238
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,44 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.78.0] — 2026-09-22
|
|
43
|
+
|
|
44
|
+
### ⚠ BREAKING
|
|
45
|
+
|
|
46
|
+
- **A file migration declares whether it runs before or after the schema apply** — `@voltro/database`, `@voltro/cli`
|
|
47
|
+
|
|
48
|
+
`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.
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **A migrate run names a skip the same run makes permanent** — `@voltro/database`, `@voltro/cli`
|
|
53
|
+
|
|
54
|
+
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.
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- **`latest` waits for the whole release, the dashboard included** — `@voltro/cli`
|
|
59
|
+
|
|
60
|
+
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.
|
|
61
|
+
- **A migration body guarding on `schema.tableExists` runs in a database-free test** — `@voltro/database`, `@voltro/runtime`
|
|
62
|
+
|
|
63
|
+
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.
|
|
64
|
+
|
|
65
|
+
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.
|
|
66
|
+
- **A storage change no longer reads as `text → text` in a migration plan** — `@voltro/cli`, `@voltro/database`
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
- **TypeScript 7 removed thirteen tsconfig options, not the three we named** — `@voltro/cli`
|
|
70
|
+
|
|
71
|
+
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.
|
|
72
|
+
|
|
73
|
+
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.
|
|
74
|
+
- **Workflow submission rows carry one identity shape again** — `@voltro/cli`, `@voltro/workflow`
|
|
75
|
+
|
|
76
|
+
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.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
42
80
|
## [0.77.1] — 2026-09-22
|
|
43
81
|
|
|
44
82
|
### Fixed
|
package/dist/index.d.ts
CHANGED
|
@@ -2961,6 +2961,42 @@ export declare interface DataStore {
|
|
|
2961
2961
|
* a way to hand a store somebody else's connection.
|
|
2962
2962
|
*/
|
|
2963
2963
|
run?<A, E>(effect: Effect.Effect<A, E, SqlClient.SqlClient>): Promise<A>;
|
|
2964
|
+
/**
|
|
2965
|
+
* What this store can say about its OWN shape without a catalog — the
|
|
2966
|
+
* tables it holds and the columns their rows carry.
|
|
2967
|
+
*
|
|
2968
|
+
* The inverse capability of {@link DataStore.run}. A SQL store answers a
|
|
2969
|
+
* schema question by asking the server's catalog, which is authoritative
|
|
2970
|
+
* about far more than this (types, indexes, constraints) and is therefore
|
|
2971
|
+
* the right source whenever there is a connection. A store that IS its data
|
|
2972
|
+
* has no catalog — and is not thereby ignorant: it knows exactly which
|
|
2973
|
+
* tables it holds, because holding them is all a table is there. That much
|
|
2974
|
+
* is answerable honestly, and nothing beyond it is.
|
|
2975
|
+
*
|
|
2976
|
+
* So the contract is deliberately narrow, and the narrowness is the point:
|
|
2977
|
+
*
|
|
2978
|
+
* - `table` is unqualified by whatever namespace the view is bound to, so
|
|
2979
|
+
* a tenant view reports the tenant's tables under their bare names.
|
|
2980
|
+
* - `columns` is the UNION of the keys the held rows carry. An empty
|
|
2981
|
+
* table therefore reports NO columns, and that is not a claim that it
|
|
2982
|
+
* has none — a caller must read `rowCount` before treating an absent
|
|
2983
|
+
* column as absent.
|
|
2984
|
+
* - `rowCount` is how many rows are held, so "the table is empty and says
|
|
2985
|
+
* nothing about its columns" is distinguishable from "the table has
|
|
2986
|
+
* rows and none of them carries that column".
|
|
2987
|
+
*
|
|
2988
|
+
* OPTIONAL on the interface: the in-memory store implements it (it is the
|
|
2989
|
+
* only store whose data IS its schema); SQL stores omit it, because the
|
|
2990
|
+
* catalog answers the same questions better and a second, weaker source of
|
|
2991
|
+
* truth beside it is how the two drift. Callers guard with
|
|
2992
|
+
* `if (store.storedTables)`.
|
|
2993
|
+
*
|
|
2994
|
+
* Read by the file-migration context built WITHOUT a `sql` client
|
|
2995
|
+
* (`makeFileMigrationContext`), so a migration body that opens with
|
|
2996
|
+
* `ctx.skipUnless(ctx.schema.tableExists('…'), …)` — the guard the file
|
|
2997
|
+
* migration docs ask for — runs unchanged in a database-free test.
|
|
2998
|
+
*/
|
|
2999
|
+
storedTables?(): ReadonlyArray<StoredTableShape>;
|
|
2964
3000
|
}
|
|
2965
3001
|
|
|
2966
3002
|
/** One transfer, as the inspector reads it back. */
|
|
@@ -3966,6 +4002,31 @@ export declare interface FileMigration {
|
|
|
3966
4002
|
* schema change through `ctx.schema` in a migration of its own).
|
|
3967
4003
|
*/
|
|
3968
4004
|
readonly transactional?: boolean;
|
|
4005
|
+
/**
|
|
4006
|
+
* WHERE in a migrate run this body belongs, relative to the declarative
|
|
4007
|
+
* schema apply. Required, and there is no default, because the answer cannot
|
|
4008
|
+
* be inferred from the body and getting it wrong is silent.
|
|
4009
|
+
*
|
|
4010
|
+
* - **`'before-schema'`** — runs FIRST, before the planner diffs declared
|
|
4011
|
+
* against live. This is the phase for everything a state diff cannot infer
|
|
4012
|
+
* and must not be computed against: a table split, a merge, a foreign key
|
|
4013
|
+
* dropped so the planner does not trip over it, a type change with a USING
|
|
4014
|
+
* expression. The plan is computed AFTER this body, against the shape it
|
|
4015
|
+
* left behind.
|
|
4016
|
+
* - **`'after-schema'`** — runs LAST, after the plan has been applied and the
|
|
4017
|
+
* plugin steps and change triggers have converged. This is the phase for a
|
|
4018
|
+
* body that NEEDS the new shape: a back-fill into a table the same deploy
|
|
4019
|
+
* creates, a column filled from its neighbours. It runs in the same job,
|
|
4020
|
+
* under the same failure gate, and — on a dialect that holds a deploy in
|
|
4021
|
+
* one transaction — inside the same unit as the schema it depends on.
|
|
4022
|
+
*
|
|
4023
|
+
* A back-fill declared `'before-schema'` is the mistake this field exists to
|
|
4024
|
+
* make unavailable: on the deploy that creates its table the body can only
|
|
4025
|
+
* take its own `skipUnless` guard, and the skip is recorded, so it never gets
|
|
4026
|
+
* a second chance. The runner names that case when it can see it, but a
|
|
4027
|
+
* warning after the fact is the weaker half — the phase is the answer.
|
|
4028
|
+
*/
|
|
4029
|
+
readonly phase: MigrationPhase;
|
|
3969
4030
|
/** The forward body. */
|
|
3970
4031
|
readonly up: (ctx: FileMigrationContext) => Promise<void> | Effect.Effect<void, unknown, never>;
|
|
3971
4032
|
/**
|
|
@@ -5128,6 +5189,7 @@ export declare type MergeMixinFields<Mixins extends ReadonlyArray<AnyMixin>> = M
|
|
|
5128
5189
|
* export default migration({
|
|
5129
5190
|
* id: '20260415_120000_split_address_out',
|
|
5130
5191
|
* description: 'Move users.address* into a separate addresses table.',
|
|
5192
|
+
* phase: 'before-schema',
|
|
5131
5193
|
* up: ({ sql, store, log }) => Effect.gen(function* () {
|
|
5132
5194
|
* yield* sql.unsafe(`CREATE TABLE addresses ( ... )`)
|
|
5133
5195
|
* yield* sql.unsafe(`INSERT INTO addresses (...) SELECT (...) FROM users`)
|
|
@@ -5139,6 +5201,9 @@ export declare type MergeMixinFields<Mixins extends ReadonlyArray<AnyMixin>> = M
|
|
|
5139
5201
|
*/
|
|
5140
5202
|
export declare const migration: (spec: FileMigration) => FileMigration;
|
|
5141
5203
|
|
|
5204
|
+
/** Both phases, in the order a run executes them. The one definition; readers derive. */
|
|
5205
|
+
export declare const MIGRATION_PHASES: readonly [MigrationPhase, MigrationPhase];
|
|
5206
|
+
|
|
5142
5207
|
export declare interface MigrationDefinition {
|
|
5143
5208
|
readonly __migration: true;
|
|
5144
5209
|
readonly id: string;
|
|
@@ -5182,10 +5247,13 @@ export declare class MigrationExpectationFailed extends Error {
|
|
|
5182
5247
|
/**
|
|
5183
5248
|
* A migration context was built without a SQL client — over the in-memory
|
|
5184
5249
|
* store, the shape a unit test uses — and the body reached a surface that
|
|
5185
|
-
* needs a live connection: `raw()`,
|
|
5186
|
-
*
|
|
5187
|
-
*
|
|
5188
|
-
*
|
|
5250
|
+
* needs a live connection: `raw()`, an Effect body under `ctx.transactional`
|
|
5251
|
+
* (which runs ON the connection), or a `ctx.schema` member that can only be
|
|
5252
|
+
* answered from a CATALOG — `snapshot`, `indexExists`, `foreignKeyExists` and
|
|
5253
|
+
* every schema CHANGE. `each`, `rewrite`, `expect`, `skipUnless`, `store`,
|
|
5254
|
+
* `log` and the two probes the store can answer about its own shape
|
|
5255
|
+
* (`tableExists`, `columnExists`) work without one; each refusal names what to
|
|
5256
|
+
* do instead. Also raised at construction when what was handed in as `sql` is not
|
|
5189
5257
|
* an `@effect/sql` client, or when a SQL-backed store arrives without the
|
|
5190
5258
|
* client it executes on — both are refused before a body can run rather
|
|
5191
5259
|
* than surfacing as a TypeError from inside the first helper that touches
|
|
@@ -5198,6 +5266,18 @@ export declare class MigrationNeedsSqlClient extends Error {
|
|
|
5198
5266
|
constructor(migrationId: string, operation: string, detail: string);
|
|
5199
5267
|
}
|
|
5200
5268
|
|
|
5269
|
+
/**
|
|
5270
|
+
* The two points in a migrate run at which a file-based body can execute.
|
|
5271
|
+
*
|
|
5272
|
+
* The ORDER of `'before-schema'` is what every file migration has always had
|
|
5273
|
+
* and is not negotiable: a body that drops a constraint the planner would trip
|
|
5274
|
+
* over has to run before the plan is computed, and `voltro db apply --plan`
|
|
5275
|
+
* refuses a pending one for exactly that reason. `'after-schema'` is the point
|
|
5276
|
+
* that did not exist — everything the declarative apply produces is in place,
|
|
5277
|
+
* so a body may depend on it.
|
|
5278
|
+
*/
|
|
5279
|
+
export declare type MigrationPhase = 'before-schema' | 'after-schema';
|
|
5280
|
+
|
|
5201
5281
|
/**
|
|
5202
5282
|
* Schema questions and schema operations a migration body can ask of the
|
|
5203
5283
|
* database it is running against, spelled once for every dialect.
|
|
@@ -7943,6 +8023,16 @@ export declare const stampGeneratedIds: <T extends Record<string, unknown>>(tabl
|
|
|
7943
8023
|
*/
|
|
7944
8024
|
export declare const startsWith: <RowOf = Record<string, unknown>, K extends keyof RowOf & string = keyof RowOf & string>(column: K, prefix: string) => PredicateLeaf;
|
|
7945
8025
|
|
|
8026
|
+
/** One table as a store that has no catalog can describe it. See {@link DataStore.storedTables}. */
|
|
8027
|
+
export declare interface StoredTableShape {
|
|
8028
|
+
/** The table's name, unqualified by any namespace the reporting view is bound to. */
|
|
8029
|
+
readonly table: string;
|
|
8030
|
+
/** The union of the column names the held rows carry. Empty for a table with no rows. */
|
|
8031
|
+
readonly columns: ReadonlyArray<string>;
|
|
8032
|
+
/** How many rows the store holds for this table. `0` means `columns` carries no evidence. */
|
|
8033
|
+
readonly rowCount: number;
|
|
8034
|
+
}
|
|
8035
|
+
|
|
7946
8036
|
/** The facts a framework refusal carries into `ConstraintViolation`. */
|
|
7947
8037
|
export declare interface StoreRefusal {
|
|
7948
8038
|
/** Which integrity rule the caller ran into. Mirrors
|