@voltro/database 0.46.0 → 0.47.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 +30 -0
- package/dist/index.d.ts +53 -8
- package/dist/index.js +315 -295
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,36 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.47.0] — 2026-08-22
|
|
43
|
+
|
|
44
|
+
### Changed
|
|
45
|
+
|
|
46
|
+
- **@voltro/database, @voltro/data-transfer, @voltro/cli, @voltro/plugin-storage** — A `--mode replace` no longer writes per-row history, and three things that were reported alongside it.
|
|
47
|
+
|
|
48
|
+
**Write recorders are suspended for a `replace`.** A replace SETS a state; it does not change rows, so a per-row history entry describes something that did not happen. A deployment measured what that costs: they run `versioningPlugin({ timing: 'in-transaction' })` on 70 of 80 tables, so one import wrote 242 950 history rows and doubled the write load of the most expensive run they make — and those rows were the source of refused writes they spent three rounds diagnosing.
|
|
49
|
+
|
|
50
|
+
The deciding argument is not the cost. **The import path had already decided this, and the recorder was the one layer that did not hear.** These routes write through the RAW store on purpose — no tenant scoping, no row filter, no `audit()` stamping. A recorder fired anyway because it hangs one level below the wrapper. Suspending it makes the layers agree.
|
|
51
|
+
|
|
52
|
+
Suspended for `replace` only: an `upsert` or an `append` CHANGES existing state, which is exactly what a recorder is for. Scoped per execution context rather than by a switch, because an import runs while the app serves requests and a process-global flag would silently drop recording for everything concurrent with it. And every such run SAYS what it suspended — dropping history quietly would be the same defect in a nicer costume.
|
|
53
|
+
|
|
54
|
+
This also removes the reason a staged `replace` used to fall back. A recorder is keyed by table name, so a staged write found none and the recorder never ran; rather than record inconsistently, the run took the slower path. For the deployment above that meant the staged path could never activate — permanently, on every environment.
|
|
55
|
+
|
|
56
|
+
**`voltro codegen` wrote a truncated table declaration.** It discovered tables with the walk that collects the rpc GROUP's inputs — descriptors, workflows, events, and deliberately no `*.entity.ts`. So the generated `voltro-tables.generated.d.ts` listed the framework's tables plus `actors`, which the framework injects. Measured downstream: 37 names where a `voltro dev` boot writes 117, and 299 `TS2322` errors from every `source:` naming one of their own tables. The way in was our own message — `voltro test` refuses a stale rpc group and tells you to run `voltro codegen`. Table discovery has its own walk now, and a test compares the two walks' RESULTS on a real tree rather than trusting they mean the same thing.
|
|
57
|
+
|
|
58
|
+
**The eager-relation doctor rule missed the state that is most wrong.** It resolved relations only against the tables a query already DECLARED, on the reasoning that the base is virtually always in `source:`. Measured: a query reading `projects` while declaring only `projectTeams` produced no finding at all, while the same query with the base added produced two. The base is a fact about the executor, so it is read from the executor now.
|
|
59
|
+
|
|
60
|
+
**A rollback capture says when its path shares the root filesystem.** The 409 answered "is storage configured", which is not the question — a `filesystem` provider pointed at a container directory with no volume behind it passes it and dies with the pod. What a process CAN observe is that a mounted volume is a different filesystem: `pathDurability` compares device ids, and a capture landing on the same device as `/` says so. Three-valued on purpose, and explicitly not an alarm on a development machine, where everything is one device and nothing is a pod.
|
|
61
|
+
|
|
62
|
+
**And the import records ITSELF, once.** Dropping per-row history left an operator asking "was this data imported, and when" with nothing to read, and trading too much for none is not obviously the better trade. `_voltro_data_imports` carries one row per RUN: the mode, the transport, the bundle, the SOURCE deployment's schema fingerprint, the counts, and — for the run an operator is actually looking for — the failure. It is written for a failed import as well as a finished one; a trail that only records successes goes quiet exactly when it is needed.
|
|
63
|
+
|
|
64
|
+
Best effort, unlike the interrupted-replace marker, and the difference is deliberate: the marker is a safety interlock and a run that cannot write it must not proceed, while history is valuable and not load-bearing. A target that has not been migrated yet still imports, and says the trace could not be written.
|
|
65
|
+
|
|
66
|
+
Two things it learned from being wired. The table is `.nonReactive()` — nobody subscribes to "an import happened", and a reactive bookkeeping write showed up in every suite that counts the LOAD's writes. And it is written only when the target actually DECLARES the table: the in-memory store accepts a write to any name, so an absent table cannot be detected by the write failing, and inventing the row would put a framework write into every embedder's counts.
|
|
67
|
+
|
|
68
|
+
**Measured at the reported magnitude.** 114 tables, 243 048 rows, real MariaDB, staged replace: the whole run takes 56.5s and the DESTRUCTIVE TRANSACTION takes **1.13s**. The window in which a dead process can leave a half-replaced target is the second number; before this it was the first.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
42
72
|
## [0.46.0] — 2026-08-22
|
|
43
73
|
|
|
44
74
|
### ⚠ BREAKING
|
package/dist/index.d.ts
CHANGED
|
@@ -2750,6 +2750,14 @@ export declare const deriveTypeIdPrefix: (tableName: string) => string;
|
|
|
2750
2750
|
*/
|
|
2751
2751
|
export declare const describeDriverError: (err: unknown) => string | undefined;
|
|
2752
2752
|
|
|
2753
|
+
/**
|
|
2754
|
+
* The line an operator reads.
|
|
2755
|
+
*
|
|
2756
|
+
* States the outcome first. "An import ran" is not the question — "did it
|
|
2757
|
+
* finish, and what did it bring" is.
|
|
2758
|
+
*/
|
|
2759
|
+
export declare const describeImportRun: (run: ImportRun) => string;
|
|
2760
|
+
|
|
2753
2761
|
/**
|
|
2754
2762
|
* The TYPE of every binding in a row, and never a value.
|
|
2755
2763
|
*
|
|
@@ -3573,6 +3581,22 @@ export declare type IdSchemeInput = undefined | {
|
|
|
3573
3581
|
|
|
3574
3582
|
export declare type IdSchemeKind = 'typeid' | 'ulid' | 'numeric' | 'snowflake' | 'custom';
|
|
3575
3583
|
|
|
3584
|
+
export declare const IMPORT_RUNS_TABLE = "_voltro_data_imports";
|
|
3585
|
+
|
|
3586
|
+
/** One import, as the inspector reads it back. */
|
|
3587
|
+
export declare interface ImportRun {
|
|
3588
|
+
readonly id: string;
|
|
3589
|
+
readonly startedAt: Date;
|
|
3590
|
+
readonly finishedAt: Date | null;
|
|
3591
|
+
readonly mode: string;
|
|
3592
|
+
readonly via: string;
|
|
3593
|
+
readonly bundle: string | null;
|
|
3594
|
+
readonly sourceFingerprint: string | null;
|
|
3595
|
+
readonly tablesWritten: number;
|
|
3596
|
+
readonly rowsWritten: number;
|
|
3597
|
+
readonly failure: string | null;
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3576
3600
|
export declare interface IndexAuditIssue {
|
|
3577
3601
|
readonly kind: IndexAuditIssueKind;
|
|
3578
3602
|
readonly table: string;
|
|
@@ -5583,14 +5607,6 @@ export declare interface RecordedWrite {
|
|
|
5583
5607
|
readonly procedure?: string | undefined;
|
|
5584
5608
|
}
|
|
5585
5609
|
|
|
5586
|
-
/**
|
|
5587
|
-
* Is ANY recorder interested in `table`?
|
|
5588
|
-
*
|
|
5589
|
-
* The stores call this on every write, so it is the hot path: a store with no
|
|
5590
|
-
* recorders registered pays one `Map.size` check and nothing else, and never
|
|
5591
|
-
* awaits. Keeping `routeEvent`'s cost at zero for the default configuration is
|
|
5592
|
-
* what makes shipping this as an opt-in honest.
|
|
5593
|
-
*/
|
|
5594
5610
|
export declare const recordsTable: (table: string) => boolean;
|
|
5595
5611
|
|
|
5596
5612
|
/**
|
|
@@ -6669,6 +6685,22 @@ export declare const sum: (column: string, alias?: string) => AggregateColumn;
|
|
|
6669
6685
|
|
|
6670
6686
|
export declare const sumOver: (column: string, alias?: string) => WindowBuilder;
|
|
6671
6687
|
|
|
6688
|
+
export declare const suspendWriteRecorders: <A>(fn: () => Promise<A>) => Promise<A>;
|
|
6689
|
+
|
|
6690
|
+
/**
|
|
6691
|
+
* The same suspension for a caller holding an EFFECT.
|
|
6692
|
+
*
|
|
6693
|
+
* Not `suspendWriteRecorders(() => Effect.runPromise(effect))`: that runs the
|
|
6694
|
+
* effect in a SECOND runtime, and the importer's table phase came back with no
|
|
6695
|
+
* progress at all when it was written that way. Entering and leaving the scope
|
|
6696
|
+
* around the effect keeps ONE runtime — the same shape `restrictingReadsEffect`
|
|
6697
|
+
* uses in `@voltro/runtime`, where the async-local's survival across Effect's
|
|
6698
|
+
* scheduling was measured.
|
|
6699
|
+
*
|
|
6700
|
+
* Nests, so a caller that is already suspended stays suspended on the way out.
|
|
6701
|
+
*/
|
|
6702
|
+
export declare const suspendWriteRecordersEffect: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
6703
|
+
|
|
6672
6704
|
/**
|
|
6673
6705
|
* Drain one table's over-TTL backlog. The DELETE itself is injected
|
|
6674
6706
|
* (`deleteOlderThan` returns the number of rows it removed in one batch); the
|
|
@@ -7772,6 +7804,16 @@ export declare const _voltroCdcOffsetsTable: TableLike;
|
|
|
7772
7804
|
*/
|
|
7773
7805
|
export declare const _voltroIdempotencyTable: TableLike;
|
|
7774
7806
|
|
|
7807
|
+
/**
|
|
7808
|
+
* One import, as an operator asks about it.
|
|
7809
|
+
*
|
|
7810
|
+
* Deliberately small. Everything here answers a question somebody has actually
|
|
7811
|
+
* asked while looking at unexpected data — "where did this come from", "when",
|
|
7812
|
+
* "did it finish" — and nothing here is a per-row fact, because that is the
|
|
7813
|
+
* thing this replaces.
|
|
7814
|
+
*/
|
|
7815
|
+
export declare const _voltroImportRunsTable: TableLike;
|
|
7816
|
+
|
|
7775
7817
|
/**
|
|
7776
7818
|
* `_voltro_kv` — the durable key-value store behind `ctx.kv` / the `Kv`
|
|
7777
7819
|
* service when `KV_BACKEND=database` (the default for sql apps). One row per
|
|
@@ -7949,4 +7991,7 @@ export declare interface WriteAttribution {
|
|
|
7949
7991
|
|
|
7950
7992
|
export declare type WriteRecorder = (port: TxnRecorderPort, write: RecordedWrite) => Promise<void>;
|
|
7951
7993
|
|
|
7994
|
+
/** Are recorders suspended right here? Exported so a caller can SAY so. */
|
|
7995
|
+
export declare const writeRecordersSuspended: () => boolean;
|
|
7996
|
+
|
|
7952
7997
|
export { }
|