@voltro/data-transfer 0.85.0 → 0.86.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.
@@ -0,0 +1,536 @@
1
+ import { environmentLocalNotice as e, isEnvironmentLocal as t } from "./environmentLocal.js";
2
+ import { BundleError as n, CrossDialectError as r, ImportModeError as i, IntegrityError as a, RowsRefusedError as o, SchemaDriftError as s } from "./errors.js";
3
+ import { restoreAssetsFromCas as c } from "./casAssetPhase.js";
4
+ import { makeDecompressor as ee } from "./compress.js";
5
+ import { ASSETS_DIR as l, MANIFEST_FILE as u, decodeManifest as d } from "./manifest.js";
6
+ import { reportProgress as f, withTableSpan as p } from "./progress.js";
7
+ import { readLedger as te, tableDone as m, writeLedger as h } from "./ledger.js";
8
+ import { ndjsonToRows as ne } from "./ndjson.js";
9
+ import { outsideReferenceRefusal as g, outsideReferencesInto as _ } from "./replacePreflight.js";
10
+ import { checkPortability as v, formatIssue as re } from "./crossDialect.js";
11
+ import { rowFailureDiagnosis as ie, tableFailureReason as ae } from "./rowFailureReason.js";
12
+ import { canStage as oe, createStagingSql as se, danglingReport as ce, decideStaging as le, dropStagingSql as ue, execStatements as y, findDangling as de, registerStagingClones as fe, runSwap as pe, stagedReferences as me, stagingNameFor as he, stagingUnavailable as ge } from "./stagedSwap.js";
13
+ import { captureRollback as _e, rollbackNotice as ve } from "./rollbackCapture.js";
14
+ import { checkSchemaDrift as ye } from "./schemaDrift.js";
15
+ import { Cause as b, Effect as x, Exit as S, Option as C, Stream as be } from "effect";
16
+ import { DATA_TRANSFERS_TABLE as w, REPLACE_IN_PROGRESS_TABLE as T, _voltroDataTransfersTable as xe, derivedRowId as E, ensureTableRegistered as Se, getTable as D, isNotNull as Ce, recordsTable as we, registerTable as Te, suspendWriteRecorders as Ee, suspendWriteRecordersEffect as De, unregisterTable as Oe } from "@voltro/database";
17
+ import { createReadStream as ke } from "node:fs";
18
+ import { fromNodeReadable as Ae } from "@voltro/plugin-storage";
19
+ //#region src/importer.ts
20
+ var je = 2e3, Me = 2e3, O = 200, k = /* @__PURE__ */ new Set([
21
+ "BundleError",
22
+ "CodecError",
23
+ "IntegrityError",
24
+ "CrossDialectError",
25
+ "ImportModeError",
26
+ "RowsRefusedError",
27
+ "SchemaDriftError",
28
+ "StorageError"
29
+ ]), A = (e) => {
30
+ let t = b.isCause(e) ? e : S.isExit(e) && S.isFailure(e) ? e.cause : void 0, n = t === void 0 ? [e] : t.reasons.flatMap((e) => b.isFailReason(e) ? [e.error] : b.isDieReason(e) ? [e.defect] : []);
31
+ for (let t of n) {
32
+ let n = t?._tag;
33
+ if (typeof n == "string" && k.has(n)) return t;
34
+ if (t !== e && (b.isCause(t) || S.isExit(t))) {
35
+ let e = A(t);
36
+ if (e) return e;
37
+ }
38
+ }
39
+ }, Ne = (e) => C.getOrUndefined(b.findErrorOption(e)) ?? Error(b.pretty(e)), Pe = (e) => x.tryPromise({
40
+ try: async () => {
41
+ let t = await import("node:fs/promises"), n = await import("node:path"), r = await t.readFile(n.join(e, u), "utf8");
42
+ return d(r);
43
+ },
44
+ catch: (e) => new n({
45
+ reason: `manifest read/parse failed: ${String(e?.message ?? e)}`,
46
+ path: u
47
+ })
48
+ }), Fe = (u) => {
49
+ let d;
50
+ return x.gen(function* () {
51
+ let b = yield* x.promise(() => import("node:path")), C = yield* Pe(u.bundleDir);
52
+ if (C.formatVersion !== 1) return yield* x.fail(new n({ reason: `unsupported bundle formatVersion ${C.formatVersion} (expected 1)` }));
53
+ if (u.tables !== void 0) {
54
+ let e = new Set(C.tables.map((e) => e.name)), t = u.tables.filter((t) => !e.has(t));
55
+ if (t.length > 0) return yield* x.fail(new n({ reason: `--tables names ${t.length} table(s) this bundle does not carry: ${t.join(", ")}. The bundle carries: ${[...e].sort().join(", ")}.` }));
56
+ }
57
+ let O = u.tables === void 0 ? void 0 : new Set(u.tables), k = O === void 0 ? C : {
58
+ ...C,
59
+ tables: C.tables.filter((e) => O.has(e.name))
60
+ };
61
+ if (u.targetSnapshot) {
62
+ let e = { tables: k.tables.map((e) => ({
63
+ name: e.name,
64
+ indexes: [],
65
+ columns: Object.entries(e.columnTypes).map(([e, t]) => ({
66
+ name: e,
67
+ type: t,
68
+ nullable: !0,
69
+ unique: !1,
70
+ hasDefault: !1
71
+ }))
72
+ })) }, t = ye(k.source.schemaFingerprint, u.targetSnapshot, e);
73
+ if (t.drifted) {
74
+ if (!u.force) return yield* x.fail(new s({
75
+ bundleFingerprint: t.bundleFingerprint,
76
+ targetFingerprint: t.targetFingerprint,
77
+ diff: t.diff
78
+ }));
79
+ u.onWarn?.(`schema drift: the target schema (fingerprint ${t.targetFingerprint.slice(0, 16)}) differs from the bundle's source (${t.bundleFingerprint.slice(0, 16)}); importing anyway (--force). ${t.diff.join("; ")}`);
80
+ }
81
+ }
82
+ if (u.targetDialect && u.targetDialect !== k.source.dialect) {
83
+ let e = v(k.tables, k.source.dialect, u.targetDialect).filter((e) => e.severity === "block");
84
+ if (e.length > 0 && !u.allowIncompatible) return yield* x.fail(new r({
85
+ sourceDialect: k.source.dialect,
86
+ targetDialect: u.targetDialect,
87
+ blocking: e.map(re)
88
+ }));
89
+ }
90
+ let Fe = u.verify ?? !0, j = u.mode ?? "upsert", M = u.atomic ?? j === "replace", Ie = u.onConflict ?? "skip", N = u.ledgerPath ?? b.join(u.bundleDir, ".import..ledger.json"), P = yield* te(N);
91
+ if (j === "replace" && k.scope.kind !== "all") return yield* x.fail(new i({
92
+ mode: j,
93
+ reason: `replace refuses a partial bundle (scope='${k.scope.kind}') — truncating would delete rows not in the bundle. Use --mode upsert, or re-export with full scope. To leave specific tables out of a FULL export, use \`--exclude a,b\`: it keeps the scope \`all\` and records what it left out.`
94
+ }));
95
+ if (j === "replace" && (k.scope.exclude?.length ?? 0) > 0) {
96
+ let e = k.scope.exclude;
97
+ u.onWarn?.(`this bundle was exported with ${e.length} table(s) excluded: ${e.join(", ")}.\n A replace empties and refills the tables the bundle CARRIES, so those keep whatever rows the
98
+ target already holds. That is not "everything the bundle says" — check it is what you meant.`);
99
+ }
100
+ if (j === "replace") {
101
+ let e = k.tables.map((e) => e.name), t = u.store.incomingForeignKeys ? yield* x.tryPromise({
102
+ try: () => u.store.incomingForeignKeys(e),
103
+ catch: (e) => new n({ reason: `foreign-key pre-flight failed: ${ae(e)}` })
104
+ }) : u.targetSnapshot === void 0 ? [] : _(u.targetSnapshot, e), r = [], a = /* @__PURE__ */ new Map();
105
+ for (let e of t) {
106
+ if (!a.has(e.from)) {
107
+ let t = yield* x.tryPromise({
108
+ try: () => u.store.query({
109
+ table: e.from,
110
+ predicate: void 0,
111
+ order: [],
112
+ take: void 0,
113
+ skip: void 0,
114
+ projection: []
115
+ }),
116
+ catch: () => new n({ reason: `unreadable: ${e.from}` })
117
+ }).pipe(x.map((e) => e.length), x.orElseSucceed(() => 0));
118
+ a.set(e.from, t);
119
+ }
120
+ let t = a.get(e.from) ?? 0;
121
+ t > 0 && r.push({
122
+ ...e,
123
+ rowCount: t
124
+ });
125
+ }
126
+ if (r.length > 0) return yield* x.fail(new i({
127
+ mode: j,
128
+ reason: g(r)
129
+ }));
130
+ }
131
+ if (u.dryRun === !0) return u.traceId !== void 0 && (yield* x.promise(() => u.store.update(w, u.traceId, {
132
+ finishedAt: /* @__PURE__ */ new Date(),
133
+ tablesWritten: 0,
134
+ rowsWritten: 0,
135
+ failure: null
136
+ }).then(() => void 0, () => void 0))), {
137
+ manifest: k,
138
+ tablesWritten: 0,
139
+ rowsWritten: 0,
140
+ tablesSkipped: 0,
141
+ rowsSkipped: 0,
142
+ fullyResumed: !1
143
+ };
144
+ let Le = /* @__PURE__ */ new Map();
145
+ for (let e of u.targetSnapshot?.tables ?? []) {
146
+ let t = e.columns.filter((e) => e.generatedAs !== void 0).map((e) => e.name);
147
+ t.length > 0 && Le.set(e.name, t);
148
+ }
149
+ let Re = (e, t) => {
150
+ let n = Le.get(e);
151
+ if (n === void 0) return t;
152
+ let r;
153
+ for (let e of n) e in t && (r ??= { ...t }, delete r[e]);
154
+ return r ?? t;
155
+ }, F = (e, t, n, r = !1) => {
156
+ let i = H ? he(t.name) : t.name, a = t.primaryKey, o = Be(t.name, Re(t.name, n), t.columnTypes);
157
+ return j === "append" ? Ie === "fail" ? e.insert(i, o) : e.insertIgnore(i, o, { conflictColumns: [a] }) : j === "replace" ? r ? e.upsert(i, o, { conflictColumns: [a] }) : e.insert(i, o) : e.upsert(i, o, { conflictColumns: [a] });
158
+ }, ze = u.targetDialect !== void 0 && u.targetDialect !== "memory", Be = (e, t, n) => {
159
+ if (!ze || n === void 0 || D(e) !== void 0) return t;
160
+ let r = null;
161
+ for (let [e, i] of Object.entries(n)) {
162
+ let n = t[e];
163
+ i === "json" && n != null && (r === null && (r = { ...t }), r[e] = JSON.stringify(n));
164
+ }
165
+ return r ?? t;
166
+ }, I = (e, t) => e.savepoint ? e.savepoint(t) : t(), L = 0, Ve = (e, t, n) => {
167
+ n.unclassified && (L++, !(L > 3) && u.onWarn?.(`row refused with no driver detail — table '${e.name}', row '${String(t.id ?? "?")}'. The full rendering follows so the LAYER that refused is on the record; it is logged, never returned. ${n.raw}` + (L === 3 ? " (further unclassified refusals are counted, not logged)" : "")));
168
+ }, R = (e, t, n = !0) => x.tryPromise({
169
+ try: e,
170
+ catch: (e) => ie(e)
171
+ }).pipe(x.map(() => !0), x.catch((e) => x.sync(() => (n && (t.reason = e.reason, Ve(t.entry, t.row, e)), !1)))), He = (e) => {
172
+ let t = Object.entries(e.entry.columnTypes).filter(([t, n]) => n === "reference" && e.row[t] !== null && e.row[t] !== void 0).map(([e]) => e);
173
+ if (t.length !== 0) return {
174
+ ...e.row,
175
+ ...Object.fromEntries(t.map((e) => [e, null]))
176
+ };
177
+ }, Ue = (e) => {
178
+ let t = new Set(e.map((e) => String(e.row[e.entry.primaryKey]))), n = (e) => Object.entries(e.entry.columnTypes).some(([n, r]) => {
179
+ if (r !== "reference") return !1;
180
+ let i = e.row[n];
181
+ return i != null && t.has(String(i));
182
+ }), r = e.map((e) => ({
183
+ table: e.entry.name,
184
+ id: String(e.row[e.entry.primaryKey]),
185
+ reason: e.reason,
186
+ derived: n(e)
187
+ })), i = [...r.filter((e) => !e.derived), ...r.filter((e) => e.derived)], a = /* @__PURE__ */ new Map();
188
+ for (let e of r) {
189
+ let t = a.get(e.table) ?? {
190
+ refused: 0,
191
+ primary: 0
192
+ };
193
+ t.refused++, e.derived || t.primary++, a.set(e.table, t);
194
+ }
195
+ return new o({
196
+ rows: i.slice(0, 20),
197
+ primaryCount: r.filter((e) => !e.derived).length,
198
+ totalCount: e.length,
199
+ byTable: [...a.entries()].map(([e, t]) => ({
200
+ table: e,
201
+ refused: t.refused,
202
+ primary: t.primary
203
+ })).sort((e, t) => t.refused - e.refused || e.table.localeCompare(t.table)).slice(0, 100)
204
+ });
205
+ }, We = (e, t) => x.gen(function* () {
206
+ let n = t, r = [], i = !0;
207
+ for (; n.length > 0 && i;) {
208
+ i = !1;
209
+ let t = [];
210
+ for (let r of n) (yield* R(() => I(e, () => F(e, r.entry, r.row, !0)), r)) ? i = !0 : t.push(r);
211
+ if (!i && t.length > 0) for (let n = t.length - 1; n >= 0; n--) {
212
+ let a = t[n], o = He(a);
213
+ o !== void 0 && (yield* R(() => I(e, () => F(e, a.entry, o, !0)), a, !1)) && (r.push(a), t.splice(n, 1), i = !0);
214
+ }
215
+ n = t;
216
+ }
217
+ for (let t of r) (yield* R(() => I(e, () => e.upsert(t.entry.name, Be(t.entry.name, t.row, t.entry.columnTypes), { conflictColumns: [t.entry.primaryKey] })), t)) || (n = [...n, t]);
218
+ if (n.length > 0) return yield* x.fail(Ue(n));
219
+ }), z = (() => {
220
+ let e = u.targetDialect;
221
+ return e === "postgres" || e === "mysql" || e === "mariadb" || e === "sqlite" || e === "mssql" ? e : void 0;
222
+ })(), B = le({
223
+ mode: j,
224
+ ...u.targetDialect === void 0 ? {} : { targetDialect: u.targetDialect },
225
+ ...u.targetSnapshot === void 0 ? {} : { targetSnapshot: u.targetSnapshot },
226
+ bundleTables: k.tables.map((e) => e.name),
227
+ isSkipped: t,
228
+ canRunSql: oe(u.store)
229
+ }), V = B.tables, Ge = k.tables.filter((e) => !t(e.name) && we(e.name)).map((e) => e.name), H = B.active, Ke = !1, U = 0, W = 0, qe, G, Je = 0, K = 0, q = (r, i) => x.gen(function* () {
230
+ if (j === "replace" && !H && !(i && P.truncated)) {
231
+ u.recordInterruption === !0 && (yield* x.tryPromise({
232
+ try: () => r.insert(T, {
233
+ id: E("repl", Ye),
234
+ startedAt: J,
235
+ tables: String(k.tables.length),
236
+ capture: Y?.dir ?? null,
237
+ via: u.via ?? "direct"
238
+ }),
239
+ catch: (e) => new n({ reason: `could not record that this replace started: ${String(e?.message ?? e)}\n The marker is what makes an interrupted run visible instead of silent, so the run does
240
+ not proceed without it.` })
241
+ }));
242
+ let e = [...k.tables].reverse().map((e) => e.name).filter((e) => !t(e)), a = !i, o = r.emptyTables ? () => r.emptyTables(e) : a ? async () => {
243
+ for (let e of [...k.tables].reverse()) await r.deleteMany(e.name, { where: Ce(e.primaryKey) });
244
+ } : () => r.transactional(async (e) => {
245
+ for (let t of [...k.tables].reverse()) await e.deleteMany(t.name, { where: Ce(t.primaryKey) });
246
+ });
247
+ yield* x.tryPromise({
248
+ try: o,
249
+ catch: (e) => new n({ reason: `emptying the target for --mode replace failed (nothing was deleted): ${ae(e)}` })
250
+ }), i && (P.truncated = !0, yield* h(N, P));
251
+ }
252
+ let o = [], s = [], c = [], l = [], d = k.tables.length;
253
+ for (let e = 0; e < k.tables.length; e++) {
254
+ let n = k.tables[e];
255
+ if (t(n.name)) {
256
+ l.push(n.name);
257
+ continue;
258
+ }
259
+ if (i && n.name in P.tables) {
260
+ c.push(n.name), K++, Je += P.tables[n.name].rowCount, f(u.onProgress, {
261
+ phase: "import",
262
+ table: n.name,
263
+ index: e,
264
+ tableCount: d,
265
+ event: "done",
266
+ rowsDone: P.tables[n.name].rowCount,
267
+ total: n.rowCount
268
+ });
269
+ continue;
270
+ }
271
+ let te = b.join(u.bundleDir, n.file);
272
+ f(u.onProgress, {
273
+ phase: "import",
274
+ table: n.name,
275
+ index: e,
276
+ tableCount: d,
277
+ event: "start",
278
+ rowsDone: 0,
279
+ total: n.rowCount
280
+ });
281
+ let m = o.length, g = x.gen(function* () {
282
+ let { createHash: e } = yield* x.promise(() => import("node:crypto")), t = e("sha256"), i = 0, a = Ae("bundle", () => ke(te).pipe(ee(k.compression))).pipe(be.tap((e) => x.sync(() => t.update(e)))), s = async (e) => {
283
+ let t = {
284
+ entry: n,
285
+ row: e,
286
+ reason: ""
287
+ };
288
+ try {
289
+ await I(r, () => F(r, n, e));
290
+ } catch (r) {
291
+ let i = ie(r);
292
+ t.reason = i.reason, Ve(n, e, i), o.push(t);
293
+ }
294
+ }, c = yield* x.promise(async () => {
295
+ let e = u.copyLoader;
296
+ if (!e || e.dialect !== "postgres") return !1;
297
+ if (M) return Ke || (Ke = !0, u.onWarn?.("bulk COPY is off for this run: it needs its own connection and this import runs in ONE transaction (the default for --mode replace, so a failure mid-load cannot leave the target emptied and unfilled). Pass --no-atomic to trade that guarantee back for COPY.")), !1;
298
+ if (j === "replace") return !0;
299
+ if (j !== "upsert") return !1;
300
+ try {
301
+ return (await r.query({
302
+ table: n.name,
303
+ predicate: void 0,
304
+ order: [],
305
+ take: 1,
306
+ skip: void 0,
307
+ projection: void 0
308
+ })).length === 0;
309
+ } catch {
310
+ return !1;
311
+ }
312
+ }), l = Object.keys(n.columnTypes), d = Math.max(1, u.copyBatchSize ?? 5e3), f = [], p = async () => {
313
+ if (f.length === 0) return;
314
+ let e = f;
315
+ f = [];
316
+ try {
317
+ await u.copyLoader.copyInto({
318
+ table: n.name,
319
+ columns: l,
320
+ columnTypes: n.columnTypes,
321
+ rows: e
322
+ });
323
+ } catch (t) {
324
+ c = !1, u.onWarn?.(`bulk COPY into ${n.name} failed (${String(t?.message ?? t)}); falling back to per-row writes for this table`);
325
+ for (let t of e) await s(t);
326
+ }
327
+ }, m = Math.max(1, u.savepointBatchSize ?? 200), h = typeof r.savepoint == "function", g = [], _ = async () => {
328
+ if (g.length === 0) return;
329
+ let e = g;
330
+ g = [];
331
+ try {
332
+ await r.savepoint(async () => {
333
+ for (let t of e) await F(r, n, t);
334
+ });
335
+ } catch {
336
+ for (let t of e) await s(t);
337
+ }
338
+ };
339
+ return yield* be.runForEach(ne(a, n.name, n.columnTypes), (e) => x.promise(async () => {
340
+ i++, G?.(), c ? (f.push(e), f.length >= d && await p()) : h ? (g.push(e), g.length >= m && await _()) : await s(e);
341
+ })), yield* x.promise(_), yield* x.promise(p), {
342
+ rowCount: i,
343
+ checksum: t.digest("hex")
344
+ };
345
+ }), { rowCount: _, checksum: v } = yield* p("import", n.name, e, d, (e) => e.rowCount, g);
346
+ if (Fe) {
347
+ if (v !== n.checksum) return yield* x.fail(new a({
348
+ what: `table ${n.name} checksum`,
349
+ expected: n.checksum,
350
+ actual: v
351
+ }));
352
+ if (_ !== n.rowCount) return yield* x.fail(new a({
353
+ what: `table ${n.name} rowCount`,
354
+ expected: String(n.rowCount),
355
+ actual: String(_)
356
+ }));
357
+ }
358
+ f(u.onProgress, {
359
+ phase: "import",
360
+ table: n.name,
361
+ index: e,
362
+ tableCount: d,
363
+ event: "done",
364
+ rowsDone: _,
365
+ total: n.rowCount
366
+ }), W++, U += _, qe?.(), G?.(), i && (o.length > m ? s.push({
367
+ name: n.name,
368
+ rowCount: _
369
+ }) : (P.tables[n.name] = {
370
+ rowCount: _,
371
+ checksum: n.checksum
372
+ }, yield* h(N, P)));
373
+ }
374
+ if (o.length > 0 && (yield* We(r, o)), j === "replace" && (yield* x.promise(() => r.deleteMany(T, { where: {
375
+ column: "startedAt",
376
+ op: "lte",
377
+ value: J
378
+ } }).then(() => void 0, () => void 0))), i && s.length > 0) {
379
+ for (let e of s) {
380
+ let t = k.tables.find((t) => t.name === e.name);
381
+ P.tables[e.name] = {
382
+ rowCount: e.rowCount,
383
+ checksum: t.checksum
384
+ };
385
+ }
386
+ yield* h(N, P);
387
+ }
388
+ {
389
+ let t = e(l, "skipped");
390
+ t !== void 0 && u.onWarn?.(t);
391
+ }
392
+ if (c.length > 0) {
393
+ let e = c.length === k.tables.length;
394
+ u.onWarn?.(`resume: ${c.length} of ${k.tables.length} table(s) were already applied by an earlier run of THIS bundle directory, so this run wrote NO rows for them${e ? " — that is every table in the bundle, so nothing was written at all" : ""}. Tables: ${c.join(", ")}. Delete ${N} to force a full re-import.`);
395
+ }
396
+ }), Ye = `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`, J = /* @__PURE__ */ new Date(), Y = yield* _e({
397
+ store: u.store,
398
+ mode: j,
399
+ dir: u.rollbackDir,
400
+ snapshot: u.targetSnapshot,
401
+ dialect: u.targetDialect,
402
+ tables: k.tables.map((e) => e.name).filter((e) => !t(e)),
403
+ onWarn: u.onWarn
404
+ });
405
+ Y !== void 0 && (u.onRollbackCaptured !== void 0 && (yield* x.tryPromise({
406
+ try: () => u.onRollbackCaptured(Y),
407
+ catch: (e) => new n({
408
+ reason: `the rollback capture could not be stored durably, so the import did not start and the target is untouched: ${String(e?.message ?? e)}\n The capture only helps if it outlives this process — that is the failure it exists for.`,
409
+ path: Y.dir
410
+ })
411
+ })), u.onWarn?.(ve(Y)));
412
+ let Xe = (e) => j === "replace" ? Ee(e) : e(), X = (e) => j === "replace" ? De(e) : e, Z = u.traceId ?? E("dimp", `${Date.now()}-${k.source.schemaFingerprint ?? ""}`), Q = u.traceId !== void 0, $ = (u.targetSnapshot?.tables.some((e) => e.name === w) ?? !1) || D(w) !== void 0;
413
+ $ && Se(xe), $ && u.traceId === void 0 && (yield* x.promise(() => u.store.insert(w, {
414
+ id: Z,
415
+ mode: j,
416
+ via: u.via ?? "direct",
417
+ direction: "import",
418
+ bundle: u.bundleDir,
419
+ sourceFingerprint: k.source.schemaFingerprint ?? null,
420
+ tablesWritten: 0,
421
+ rowsWritten: 0,
422
+ staged: j === "replace" ? H : null
423
+ }).then(() => {
424
+ Q = !0;
425
+ }, (e) => {
426
+ u.onWarn?.(`the import trace could not be written (${String(e?.message ?? e)}).\n The import continues — this is a record of what happened, not a safety check. Run
427
+ \`voltro db apply\` so the next one is recorded.`);
428
+ }))), u.traceId !== void 0 && $ && (yield* x.promise(() => u.store.update(w, Z, {
429
+ staged: j === "replace" ? H : null,
430
+ sourceFingerprint: k.source.schemaFingerprint ?? null
431
+ }).then(() => void 0, () => void 0)));
432
+ let Ze = 0, Qe = !1;
433
+ qe = () => {
434
+ if (!Q || Qe) return;
435
+ let e = Date.now();
436
+ e - Ze < je || (Ze = e, u.store.update(w, Z, {
437
+ tablesWritten: W,
438
+ rowsWritten: U
439
+ }).then(() => void 0, () => {
440
+ Qe = !0;
441
+ }));
442
+ };
443
+ let $e = (e) => Q ? u.store.update(w, Z, {
444
+ finishedAt: e === null ? /* @__PURE__ */ new Date() : null,
445
+ tablesWritten: W,
446
+ rowsWritten: U,
447
+ failure: e
448
+ }).then(() => void 0, () => void 0) : Promise.resolve();
449
+ if (d = (e) => $e(e), j === "replace" && !H && B.blocker !== void 0 && u.onWarn?.(ge(B.blocker)), j === "replace" && Ge.length > 0 && u.onWarn?.(`write recorders are SUSPENDED for this replace (${Ge.length} table(s) have one).\n A replace sets a state rather than changing rows, so a per-row history entry would describe
450
+ something that did not happen — and this path already writes through the raw store, without
451
+ tenant scoping or \`audit()\` stamping, for the same reason.`), H) {
452
+ u.onWarn?.(`staging ${V.length} table(s) before the swap — the target keeps its rows until the load stands.\n The destructive step is one server-side transaction at the end, not the whole load.`);
453
+ let e = u.store, t = V.map((e) => e.name), r = E("repl", `staging-${Ye}`), i = !1;
454
+ if (u.recordInterruption === !0) {
455
+ yield* x.promise(() => u.store.insert(T, {
456
+ id: r,
457
+ startedAt: J,
458
+ kind: "staging",
459
+ tables: String(V.length),
460
+ capture: Y?.dir ?? null,
461
+ via: u.via ?? "direct",
462
+ staging: t.map(he).join(","),
463
+ heartbeatAt: /* @__PURE__ */ new Date(),
464
+ resumable: !M
465
+ }).then(() => {
466
+ i = !0;
467
+ }, (e) => {
468
+ u.onWarn?.(`the staging tables could not be recorded (${String(e?.message ?? e)}).\n The import continues and the target is unaffected — but if this run dies before the swap,
469
+ nothing at boot will mention the scratch tables it leaves. \`voltro data clear-staging\` finds
470
+ them by introspection.`);
471
+ }));
472
+ let e = Date.now();
473
+ G = () => {
474
+ if (!i) return;
475
+ let t = Date.now();
476
+ t - e < Me || (e = t, u.store.update(T, r, { heartbeatAt: /* @__PURE__ */ new Date() }).then(() => void 0, () => void 0));
477
+ };
478
+ }
479
+ let a = () => i ? u.store.delete(T, r).then(() => void 0, () => void 0) : Promise.resolve(), o = fe(t, {
480
+ getTable: (e) => D(e),
481
+ registerTable: (e) => {
482
+ Te(e);
483
+ },
484
+ unregisterTable: Oe
485
+ }), s = !1;
486
+ try {
487
+ let r = M ? t : t.filter((e) => !m(P, e));
488
+ yield* x.tryPromise({
489
+ try: async () => {
490
+ await y(e, r.map((e) => ue(e, z))), await y(e, r.map((e) => se(e, z)));
491
+ },
492
+ catch: (e) => new n({ reason: `could not prepare the staging tables, so the import did not start and the target is untouched: ${String(e?.message ?? e)}` })
493
+ }), yield* X(q(u.store, !M)), !M && P.truncated === !0 ? s = !0 : yield* x.tryPromise({
494
+ try: () => pe(e, V, z).then(() => {
495
+ s = !0;
496
+ }),
497
+ catch: (e) => new n({ reason: String(e?.message ?? e) })
498
+ }).pipe(x.catch((r) => x.gen(function* () {
499
+ let i = yield* x.promise(() => de(e, me(u.targetSnapshot, t), z).then((e) => e, () => [])), a = ce(i);
500
+ return yield* x.fail(new n({ reason: a ?? `the swap failed and the target is UNCHANGED: ${r.reason}` }));
501
+ }))), !M && s && P.truncated !== !0 && (P.truncated = !0, yield* h(N, P));
502
+ } finally {
503
+ o.undo(), s || M ? (G = void 0, yield* x.promise(() => y(e, t.map((e) => ue(e, z))).then(() => void 0, () => void 0)), yield* x.promise(a)) : u.onWarn?.("the staged rows are KEPT so a re-run can continue from them rather than reload.\n If you are not going to re-run this bundle, drop them: voltro data clear-staging --yes");
504
+ }
505
+ } else M ? yield* x.tryPromise({
506
+ try: () => u.store.transactional(async (e) => {
507
+ let t = await Xe(() => x.runPromiseExit(q(e, !1)));
508
+ if (S.isFailure(t)) throw Ne(t.cause);
509
+ }),
510
+ catch: (e) => A(e) ?? new n({ reason: `atomic import failed (rolled back): ${String(e?.message ?? e)}` })
511
+ }) : yield* X(q(u.store, !0));
512
+ if (u.assets && k.assets.index && !u.assetsAlreadyRestored) {
513
+ let e = u.assets, t = b.join(u.bundleDir, l), n = b.join(u.bundleDir, k.assets.index ?? "assets/index.ndjson");
514
+ yield* c({
515
+ sink: e,
516
+ assetsDir: t,
517
+ indexPath: n,
518
+ progress: {
519
+ done: (e) => e in P.assets,
520
+ mark: (e) => (P.assets[e] = !0, h(N, P))
521
+ }
522
+ });
523
+ }
524
+ return yield* x.promise(() => $e(null)), {
525
+ manifest: k,
526
+ tablesWritten: W,
527
+ rowsWritten: U,
528
+ tablesSkipped: K,
529
+ rowsSkipped: Je,
530
+ fullyResumed: W === 0 && K > 0,
531
+ ...Y === void 0 ? {} : { rollback: Y }
532
+ };
533
+ }).pipe(x.tapError((e) => x.promise(() => d?.(e._tag === "RowsRefusedError" ? e.message : e?.reason ?? e?._tag ?? "the import did not finish") ?? Promise.resolve())));
534
+ };
535
+ //#endregion
536
+ export { O as SAVEPOINT_BATCH_SIZE, A as asImportError, Fe as runImport };
package/dist/index.d.ts CHANGED
@@ -1563,6 +1563,8 @@ declare interface RollbackCapture {
1563
1563
  * list sorted by nothing sees hundreds of lines of which one matters.
1564
1564
  */
1565
1565
  export declare class RowsRefusedError extends RowsRefusedError_base {
1566
+ /** Preserve the bounded refusal report when an error is stored as text. */
1567
+ get message(): string;
1566
1568
  }
1567
1569
 
1568
1570
  declare const RowsRefusedError_base: Schema.Class<RowsRefusedError, Schema.TaggedStruct<"RowsRefusedError", {