@voltro/data-transfer 0.84.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.
- package/CHANGELOG.md +1215 -0
- package/THIRD-PARTY-NOTICES.md +91 -365
- package/dist/assets.js +47 -0
- package/dist/blobStorage.js +40 -0
- package/dist/bundleArchive.js +165 -0
- package/dist/bundleCrypto.js +106 -0
- package/dist/casAssetPhase.js +43 -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 +122 -0
- package/dist/errors.js +82 -0
- package/dist/exporter.js +257 -0
- package/dist/importer.js +536 -0
- package/dist/index.d.ts +118 -121
- package/dist/index.js +28 -2578
- package/dist/ledger.js +21 -0
- package/dist/liveGuard.js +11 -0
- package/dist/manifest.js +52 -0
- package/dist/masking.js +246 -0
- package/dist/maskingPreview.js +48 -0
- package/dist/nativeDump.js +369 -0
- package/dist/ndjson.js +13 -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/stagedSwap.js +164 -0
- package/dist/subset.js +90 -0
- package/dist/topo.js +24 -0
- package/dist/typedCodec.js +36 -0
- package/package.json +4 -5
package/dist/importer.js
ADDED
|
@@ -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 };
|