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