@voltro/data-transfer 0.43.2 → 0.44.1
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 +50 -0
- package/dist/index.d.ts +51 -1
- package/dist/index.js +435 -392
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -39,6 +39,56 @@ _Changes staged for the next release accumulate here (rolled up from
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## [0.44.1] — 2026-08-19
|
|
43
|
+
|
|
44
|
+
### Fixed
|
|
45
|
+
|
|
46
|
+
- **@voltro/database, @voltro/data-transfer, @voltro/cli** — A refused import row reported `(FiberFailure) SqlError: Failed to execute statement` — our runtime's rendering of a rejection, marker and stack frames and all — instead of the constraint that fired. It names neither a rule, nor a code, nor even which layer refused, and every row refused for the same cause carries it identically.
|
|
47
|
+
|
|
48
|
+
Two defects, and fixing either alone still leaves a reader stuck.
|
|
49
|
+
|
|
50
|
+
**`Cause.squash` elects a branch, and first is a position, not a ranking.** A `Cause` is a tree, and a transaction routinely produces one with more than one leaf: the statement that failed, and whatever the rollback or a finalizer did on the way out. When the first leaf was the bare wrapper, the driver error sitting in the sibling branch was never looked at. Measured: `sequential(bareSqlError, sqlErrorWithDriver)` classified as nothing while the same two branches in the opposite order classified as `unique constraint PRIMARY … [ER_DUP_ENTRY/1062]`. `extractDbCause` flattens failures AND defects now, in Cause order, expanding a nested `FiberFailure` leaf, and elects the branch that names a driver — the others' chains are appended rather than dropped.
|
|
51
|
+
|
|
52
|
+
**A row's reason may never read like a stack trace.** The runtime rendering is stripped before any tier looks at the text, so the failure mode cannot return invisibly. And when no driver detail is reachable at all, the reason now names the CHAIN of wrappers the failure passed through — the difference between "the database refused this row" and "the connection died mid-import" — while the run logs the full rendering of the first few such failures. Never returned over the wire: it carries frames, and on some engines a driver's sentence carries row data.
|
|
53
|
+
|
|
54
|
+
`RowsRefusedError` also gained `byTable`: **complete** per-table counts. `rows` is capped at 20, so per-table counts tallied off the printed list sum to the cap rather than to the failure — and nothing else in the payload offered any.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## [0.44.0] — 2026-08-19
|
|
59
|
+
|
|
60
|
+
### ⚠ BREAKING
|
|
61
|
+
|
|
62
|
+
- **@voltro/data-transfer, @voltro/cli** — `runImport` returns an `ImportOutcome` instead of the bundle's `Manifest`, and `--mode replace` runs as ONE transaction by default.
|
|
63
|
+
|
|
64
|
+
**Why the return type changed.** The summary line counted the rows the BUNDLE carries, not the rows the run wrote. Those differ most exactly where it matters: a bundle directory carries its own resume ledger, so a copied directory imports nothing — correctly, with a warning naming the file to delete — and the run then printed `import complete … rows: 242950` over a target it had not touched. The warning was one line above, which is one line too far for anyone piping the output through `tail -1`. The outcome carries `rowsWritten`, `rowsSkipped` and `fullyResumed`, the CLI reports written-vs-carried, and a fully-skipped run says so on its LAST line.
|
|
65
|
+
|
|
66
|
+
Migration: `runImport(...)` now resolves to `{ manifest, tablesWritten, rowsWritten, tablesSkipped, rowsSkipped, fullyResumed }`. Read `.manifest` where you read the manifest before.
|
|
67
|
+
|
|
68
|
+
**Why replace is atomic now.** The all-or-nothing guarantee was written for the emptying step, and read — reasonably — as covering the run. A replace that died partway through the LOAD left the target emptied of its old rows and holding part of the new ones, measured on a live instance over sixteen minutes. There is no useful state for a replace to stop in, so it is a default rather than a flag you have to know about. It also closes the window that produced that failure: with the tables emptied and the load uncommitted, a concurrent writer in the application waits instead of inserting a row the bundle is about to insert too.
|
|
69
|
+
|
|
70
|
+
`--no-atomic` (CLI) / `atomic: false` (API) opts out. The trade is stated where it bites: every write to those tables waits for the load, and on postgres the bulk `COPY` loader cannot join a transaction it does not own — an atomic run now says that once rather than being quietly slower.
|
|
71
|
+
|
|
72
|
+
Two smaller things from the same report: `--mode replace` against a running instance warns about the empty-target window when it is NOT atomic, and the `--target api` upload reports progress per chunk plus a line explaining that the final request stays open for the whole import — sixteen minutes of silence is indistinguishable from a hang, and one operator killed a run that had finished.
|
|
73
|
+
|
|
74
|
+
**`voltro update` carries you across this** — codemod `0.44.0/01_import-outcome`.
|
|
75
|
+
|
|
76
|
+
### Fixed
|
|
77
|
+
|
|
78
|
+
- **@voltro/sql-mysql, @voltro/sql-postgres, @voltro/sql-sqlite, @voltro/sql-mssql, @voltro/data-transfer** — A write and its write-recorders now succeed or fail together on every SQL dialect, and the data importer's retry is idempotent.
|
|
79
|
+
|
|
80
|
+
A recorder (a versioning trail, an audit log) runs on the caller's connection and is ALLOWED to fail — a recorder that throws must take the write down with it, that is its contract. Outside a caller transaction the two were not one unit: the row's statement committed on its own, and the recorder's INSERT ran afterwards as a second autocommit statement. So a recorder that threw left a COMMITTED row behind a write that reported failure. Measured directly: `insert` throws, the row is in the table, and a second attempt at the same row is `ER_DUP_ENTRY` on PRIMARY.
|
|
81
|
+
|
|
82
|
+
Anything that retries a failed write then meets its own row. The data importer retries by design — it holds a row whose write failed and tries again once the remaining tables have streamed — so a `--mode replace` that had just emptied a table failed on a duplicate key IN that table. From outside, that is the impossible-looking thing: an import that emptied a table and then failed because something was already in it. Reproduced verbatim, including the table name and `[ER_DUP_ENTRY/1062]`, by putting the old retry back.
|
|
83
|
+
|
|
84
|
+
Both ends are closed, and they are independent on purpose:
|
|
85
|
+
|
|
86
|
+
- **The cause.** A table that HAS recorders writes inside a transaction now, so the row and the trail commit together or not at all — on mysql, mariadb, postgres, sqlite and mssql, verified by one suite that asks all five the same question. A table with no recorders — the default — takes the direct path unchanged; `recordsTable` is a Map-size check first, so it costs one comparison. - **The defence.** The importer's retry upserts instead of inserting in `replace` mode. That covers every OTHER way a write can land while reporting failure: a driver timeout on a write the server applied, a connection lost after the commit, a concurrent writer inserting the same key. It is sound precisely because the table was emptied by this same run — there is nothing in it that is not ours.
|
|
87
|
+
|
|
88
|
+
The test that pinned the divergence went red when the fix landed, exactly as its own note said it would, and is inverted with that note kept.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
42
92
|
## [0.43.2] — 2026-08-18
|
|
43
93
|
|
|
44
94
|
### Fixed
|
package/dist/index.d.ts
CHANGED
|
@@ -570,6 +570,10 @@ export declare interface ImportOptions {
|
|
|
570
570
|
* `replace`. Trade-offs: one long-held write transaction (writes to those
|
|
571
571
|
* tables block for the load; readers are unaffected on MVCC dialects), and
|
|
572
572
|
* resume is not per-table (a crash rolls the whole thing back → re-run).
|
|
573
|
+
*
|
|
574
|
+
* DEFAULTS TO TRUE FOR `replace`, and to false otherwise. Omit it to get that
|
|
575
|
+
* rule; pass `false` to opt out deliberately. See where it is resolved in
|
|
576
|
+
* `runImport` for why replace cannot sensibly run without it.
|
|
573
577
|
*/
|
|
574
578
|
readonly atomic?: boolean;
|
|
575
579
|
/** The streaming import path (`unpackBundle` with a storage {@link AssetSink})
|
|
@@ -648,6 +652,33 @@ export declare interface ImportOptions {
|
|
|
648
652
|
readonly savepointBatchSize?: number;
|
|
649
653
|
}
|
|
650
654
|
|
|
655
|
+
/**
|
|
656
|
+
* What a run actually did — as opposed to what the bundle contains.
|
|
657
|
+
*
|
|
658
|
+
* The two are not the same, and reporting the bundle's numbers as the run's is a
|
|
659
|
+
* lie of the same family this package keeps finding: a re-run of a bundle
|
|
660
|
+
* directory whose ledger says every table is applied writes NOTHING and used to
|
|
661
|
+
* be summarised as `import complete … rows: 242950`. The warning that explained
|
|
662
|
+
* it was one line further up, which is one line too far for anyone who pipes the
|
|
663
|
+
* output through `tail`.
|
|
664
|
+
*
|
|
665
|
+
* `rowsWritten` counts rows this run streamed into the target. `rowsSkipped`
|
|
666
|
+
* counts rows in tables an earlier run of THIS bundle directory had already
|
|
667
|
+
* applied. A dry run reports zero written and nothing skipped: it examined
|
|
668
|
+
* everything and wrote nothing.
|
|
669
|
+
*/
|
|
670
|
+
export declare interface ImportOutcome {
|
|
671
|
+
/** The bundle's own contract — what it CARRIES. */
|
|
672
|
+
readonly manifest: Manifest;
|
|
673
|
+
readonly tablesWritten: number;
|
|
674
|
+
readonly rowsWritten: number;
|
|
675
|
+
/** Tables an earlier run of this bundle directory already applied. */
|
|
676
|
+
readonly tablesSkipped: number;
|
|
677
|
+
readonly rowsSkipped: number;
|
|
678
|
+
/** True when nothing was written because everything was already applied. */
|
|
679
|
+
readonly fullyResumed: boolean;
|
|
680
|
+
}
|
|
681
|
+
|
|
651
682
|
/** An integrity check failed — a table's content checksum or row count did not
|
|
652
683
|
* match the manifest, or an asset's bytes did not hash to its recorded id. */
|
|
653
684
|
export declare class IntegrityError extends IntegrityError_base {
|
|
@@ -1107,6 +1138,25 @@ declare const RowsRefusedError_base: Schema.TaggedErrorClass<RowsRefusedError, "
|
|
|
1107
1138
|
primaryCount: typeof Schema.Number;
|
|
1108
1139
|
/** Total number of refused rows (may exceed `rows.length`). */
|
|
1109
1140
|
totalCount: typeof Schema.Number;
|
|
1141
|
+
/**
|
|
1142
|
+
* Every table that refused a row, with COMPLETE counts — not counts of the
|
|
1143
|
+
* capped `rows` list.
|
|
1144
|
+
*
|
|
1145
|
+
* `rows` is capped at 20, and the shape of a report invites tallying what it
|
|
1146
|
+
* shows — the per-table counts read off that list always sum to the cap, and
|
|
1147
|
+
* nothing else in the payload offered any. Which tables refused, and how
|
|
1148
|
+
* badly, is the first question of a triage and the one the cap silently
|
|
1149
|
+
* answers wrong, so it is its own field: computed over all of them, ordered
|
|
1150
|
+
* worst-first and capped separately (a bundle can carry thousands of tables;
|
|
1151
|
+
* a hundred failing ones is already a different conversation).
|
|
1152
|
+
*/
|
|
1153
|
+
byTable: Schema.Array$<Schema.Struct<{
|
|
1154
|
+
table: typeof Schema.String;
|
|
1155
|
+
/** Rows of this table that were refused. */
|
|
1156
|
+
refused: typeof Schema.Number;
|
|
1157
|
+
/** …of which are not a consequence of another refused row. */
|
|
1158
|
+
primary: typeof Schema.Number;
|
|
1159
|
+
}>>;
|
|
1110
1160
|
}>;
|
|
1111
1161
|
|
|
1112
1162
|
/** Row stream → NDJSON byte stream (each row → typed-encoded JSON + `\n`). The
|
|
@@ -1115,7 +1165,7 @@ export declare const rowsToNdjson: <E, R>(rows: Stream.Stream<Row, E, R>, column
|
|
|
1115
1165
|
|
|
1116
1166
|
export declare const runExport: (opts: ExportOptions) => Effect.Effect<Manifest, ExportError>;
|
|
1117
1167
|
|
|
1118
|
-
export declare const runImport: (opts: ImportOptions) => Effect.Effect<
|
|
1168
|
+
export declare const runImport: (opts: ImportOptions) => Effect.Effect<ImportOutcome, ImportError>;
|
|
1119
1169
|
|
|
1120
1170
|
/** Execute a {@link NativeStep} — spawn the tool (wiring stdin/stdout files) or
|
|
1121
1171
|
* copy the file. Maps a missing binary / non-zero exit to {@link NativeToolError}. */
|