admindb 2.1.0 → 2.2.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/README.md +41 -12
- package/dist/app.d.ts +1 -69
- package/dist/app.js +3 -30
- package/dist/auth/config.d.ts +1 -19
- package/dist/auth/config.js +0 -26
- package/dist/auth/crypto.d.ts +1 -27
- package/dist/auth/crypto.js +1 -26
- package/dist/auth/index.d.ts +1 -5
- package/dist/auth/index.js +0 -1
- package/dist/auth/middleware.d.ts +1 -14
- package/dist/auth/middleware.js +0 -15
- package/dist/auth/routes.d.ts +1 -7
- package/dist/auth/routes.js +0 -8
- package/dist/auth/types.d.ts +1 -35
- package/dist/auth/types.js +1 -5
- package/dist/cli/args.d.ts +1 -45
- package/dist/cli/args.js +3 -11
- package/dist/cli/config.d.ts +1 -56
- package/dist/cli/config.js +0 -5
- package/dist/cli/index.d.ts +1 -4
- package/dist/cli/index.js +0 -1
- package/dist/cli/runner.d.ts +1 -1
- package/dist/cli/runner.js +6 -17
- package/dist/cli.d.ts +1 -2
- package/dist/cli.js +0 -1
- package/dist/data/chain.d.ts +1 -0
- package/dist/data/chain.js +707 -0
- package/dist/data/datasets.d.ts +1 -17
- package/dist/data/datasets.js +0 -4
- package/dist/data/detector.d.ts +1 -30
- package/dist/data/detector.js +53 -14
- package/dist/data/engine.d.ts +1 -10
- package/dist/data/engine.js +82 -48
- package/dist/data/index.d.ts +1 -5
- package/dist/data/index.js +1 -1
- package/dist/data/strategies.d.ts +1 -30
- package/dist/data/strategies.js +4 -8
- package/dist/data/types.d.ts +1 -73
- package/dist/data/types.js +0 -3
- package/dist/db/database.d.ts +1 -119
- package/dist/db/database.js +20 -14
- package/dist/db/export.d.ts +1 -12
- package/dist/db/export.js +54 -6
- package/dist/db/filters.d.ts +1 -22
- package/dist/db/filters.js +0 -16
- package/dist/db/index.d.ts +1 -8
- package/dist/db/index.js +0 -1
- package/dist/db/introspection.d.ts +1 -6
- package/dist/db/introspection.js +0 -5
- package/dist/db/manager.d.ts +1 -81
- package/dist/db/manager.js +13 -59
- package/dist/db/migrations.d.ts +1 -5
- package/dist/db/migrations.js +2 -5
- package/dist/db/postgres.d.ts +1 -85
- package/dist/db/postgres.js +21 -31
- package/dist/db/types.d.ts +1 -206
- package/dist/db/types.js +0 -1
- package/dist/index.d.ts +1 -23
- package/dist/index.js +0 -14
- package/dist/public/css/app.css +1 -1
- package/dist/public/js/api.js +1 -47
- package/dist/public/js/app.js +1 -262
- package/dist/public/js/browse.js +1 -1371
- package/dist/public/js/common.js +1 -0
- package/dist/public/js/databases.js +1 -235
- package/dist/public/js/designer.js +1 -258
- package/dist/public/js/forms.js +1 -826
- package/dist/public/js/home.js +1 -0
- package/dist/public/js/inspector.js +1 -638
- package/dist/public/js/query.js +1 -322
- package/dist/public/js/schema.js +1 -447
- package/dist/public/js/seed.js +1 -1090
- package/dist/routes/api/erd.d.ts +1 -0
- package/dist/routes/api/erd.js +50 -0
- package/dist/routes/api/helpers.d.ts +1 -42
- package/dist/routes/api/helpers.js +33 -3
- package/dist/routes/api/import-export.d.ts +1 -3
- package/dist/routes/api/import-export.js +0 -2
- package/dist/routes/api/index.d.ts +1 -4
- package/dist/routes/api/index.js +15 -4
- package/dist/routes/api/query.d.ts +1 -3
- package/dist/routes/api/query.js +13 -11
- package/dist/routes/api/rows.d.ts +1 -3
- package/dist/routes/api/rows.js +0 -11
- package/dist/routes/api/seed.d.ts +1 -3
- package/dist/routes/api/seed.js +79 -8
- package/dist/routes/api/tables.d.ts +1 -3
- package/dist/routes/api/tables.js +77 -6
- package/dist/routes/databases.d.ts +1 -26
- package/dist/routes/databases.js +1 -3
- package/dist/routes/index.d.ts +1 -3
- package/dist/routes/index.js +0 -1
- package/dist/routes/pages.d.ts +1 -9
- package/dist/routes/pages.js +18 -11
- package/dist/serverless.d.ts +1 -40
- package/dist/serverless.js +3 -39
- package/dist/sql/classifier.d.ts +1 -19
- package/dist/sql/classifier.js +0 -13
- package/dist/sql/error-analyzer.d.ts +1 -0
- package/dist/sql/error-analyzer.js +550 -0
- package/dist/sql/generator.d.ts +1 -81
- package/dist/sql/generator.js +0 -29
- package/dist/sql/index.d.ts +1 -2
- package/dist/sql/index.js +0 -1
- package/dist/types/api.d.ts +1 -412
- package/dist/types/api.js +0 -7
- package/dist/utils/colors.d.ts +1 -40
- package/dist/utils/colors.js +0 -10
- package/dist/utils/common.d.ts +1 -50
- package/dist/utils/common.js +19 -23
- package/dist/utils/csv.d.ts +1 -15
- package/dist/utils/csv.js +0 -13
- package/dist/utils/datatype.d.ts +1 -49
- package/dist/utils/datatype.js +0 -41
- package/dist/utils/icons.d.ts +1 -11
- package/dist/utils/icons.js +25 -16
- package/dist/utils/index.d.ts +1 -4
- package/dist/utils/index.js +0 -1
- package/dist/utils/logger.d.ts +1 -10
- package/dist/utils/logger.js +0 -4
- package/dist/views/layouts/main.hbs +14 -5
- package/dist/views/pages/databases.hbs +0 -18
- package/dist/views/pages/designer.hbs +0 -10
- package/dist/views/pages/erd.hbs +1241 -0
- package/dist/views/pages/error.hbs +0 -2
- package/dist/views/pages/form.hbs +0 -5
- package/dist/views/pages/home.hbs +98 -10
- package/dist/views/pages/info.hbs +107 -0
- package/dist/views/pages/login.hbs +0 -13
- package/dist/views/pages/query.hbs +54 -21
- package/dist/views/pages/schema.hbs +0 -20
- package/dist/views/pages/seed.hbs +435 -202
- package/dist/views/pages/table.hbs +0 -14
- package/dist/views/partials/icon.hbs +0 -1
- package/dist/views/partials/navbar.hbs +0 -6
- package/dist/views/partials/sidebar.hbs +12 -8
- package/docs/API.md +6 -1
- package/docs/EXAMPLES.md +3 -0
- package/package.json +7 -5
- package/dist/app.js.map +0 -1
- package/dist/auth/config.js.map +0 -1
- package/dist/auth/crypto.js.map +0 -1
- package/dist/auth/index.js.map +0 -1
- package/dist/auth/middleware.js.map +0 -1
- package/dist/auth/routes.js.map +0 -1
- package/dist/auth/types.js.map +0 -1
- package/dist/cli/args.js.map +0 -1
- package/dist/cli/config.js.map +0 -1
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/runner.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/data/datasets.js.map +0 -1
- package/dist/data/detector.js.map +0 -1
- package/dist/data/engine.js.map +0 -1
- package/dist/data/index.js.map +0 -1
- package/dist/data/strategies.js.map +0 -1
- package/dist/data/types.js.map +0 -1
- package/dist/db/database.js.map +0 -1
- package/dist/db/export.js.map +0 -1
- package/dist/db/filters.js.map +0 -1
- package/dist/db/index.js.map +0 -1
- package/dist/db/introspection.js.map +0 -1
- package/dist/db/manager.js.map +0 -1
- package/dist/db/migrations.js.map +0 -1
- package/dist/db/postgres.js.map +0 -1
- package/dist/db/types.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/public/css/input.css +0 -302
- package/dist/public/js/icons.js +0 -51
- package/dist/routes/api/helpers.js.map +0 -1
- package/dist/routes/api/import-export.js.map +0 -1
- package/dist/routes/api/index.js.map +0 -1
- package/dist/routes/api/query.js.map +0 -1
- package/dist/routes/api/rows.js.map +0 -1
- package/dist/routes/api/seed.js.map +0 -1
- package/dist/routes/api/tables.js.map +0 -1
- package/dist/routes/databases.js.map +0 -1
- package/dist/routes/index.js.map +0 -1
- package/dist/routes/pages.js.map +0 -1
- package/dist/serverless.js.map +0 -1
- package/dist/sql/classifier.js.map +0 -1
- package/dist/sql/generator.js.map +0 -1
- package/dist/sql/index.js.map +0 -1
- package/dist/types/api.js.map +0 -1
- package/dist/utils/colors.js.map +0 -1
- package/dist/utils/common.js.map +0 -1
- package/dist/utils/csv.js.map +0 -1
- package/dist/utils/datatype.js.map +0 -1
- package/dist/utils/icons.js.map +0 -1
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/logger.js.map +0 -1
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildErGraph = buildErGraph;
|
|
4
|
+
exports.resolveErChain = resolveErChain;
|
|
5
|
+
exports.topologicalSort = topologicalSort;
|
|
6
|
+
exports.getErChainConfig = getErChainConfig;
|
|
7
|
+
exports.generateChainRows = generateChainRows;
|
|
8
|
+
exports.buildChainInsertSql = buildChainInsertSql;
|
|
9
|
+
exports.executeChainInsert = executeChainInsert;
|
|
10
|
+
const generator_1 = require("../sql/generator");
|
|
11
|
+
const types_1 = require("./types");
|
|
12
|
+
const detector_1 = require("./detector");
|
|
13
|
+
const strategies_1 = require("./strategies");
|
|
14
|
+
async function sampleFkValues(db, fk) {
|
|
15
|
+
if (!fk)
|
|
16
|
+
return [];
|
|
17
|
+
try {
|
|
18
|
+
const ref = await db.getTableInfo(fk.table);
|
|
19
|
+
if (!ref.success || !ref.data || !ref.data.columns.length)
|
|
20
|
+
return [];
|
|
21
|
+
const refCol = fk.to || ref.data.primaryKey[0] || ref.data.columns[0].name;
|
|
22
|
+
if (!refCol)
|
|
23
|
+
return [];
|
|
24
|
+
const r = await db.all(`SELECT DISTINCT ${(0, generator_1.quoteIdentifier)(refCol)} AS v FROM ${(0, generator_1.quoteIdentifier)(fk.table)} WHERE ${(0, generator_1.quoteIdentifier)(refCol)} IS NOT NULL LIMIT 1000`);
|
|
25
|
+
return (r.data ?? []).map((row) => row.v).filter((v) => v != null);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function stringifyKey(v) {
|
|
32
|
+
if (v instanceof Uint8Array || Buffer.isBuffer(v))
|
|
33
|
+
return `buf:${Buffer.from(v).toString('hex')}`;
|
|
34
|
+
return `${typeof v}:${String(v)}`;
|
|
35
|
+
}
|
|
36
|
+
function orderedValue(value, base, dir, orEqual) {
|
|
37
|
+
const v = value;
|
|
38
|
+
const b = base;
|
|
39
|
+
if (typeof v === 'string' && typeof b === 'string') {
|
|
40
|
+
const tv = Date.parse(v);
|
|
41
|
+
const tb = Date.parse(b);
|
|
42
|
+
if (Number.isFinite(tv) && Number.isFinite(tb)) {
|
|
43
|
+
if (dir === 'after') {
|
|
44
|
+
if (orEqual ? tv < tb : tv <= tb)
|
|
45
|
+
return new Date(tb + 1000).toISOString();
|
|
46
|
+
}
|
|
47
|
+
else if (orEqual ? tv > tb : tv >= tb) {
|
|
48
|
+
return new Date(tb - 1000).toISOString();
|
|
49
|
+
}
|
|
50
|
+
return v;
|
|
51
|
+
}
|
|
52
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(v) && /^\d{4}-\d{2}-\d{2}$/.test(b)) {
|
|
53
|
+
const dv = Date.parse(`${v}T00:00:00Z`);
|
|
54
|
+
const db = Date.parse(`${b}T00:00:00Z`);
|
|
55
|
+
if (dir === 'after') {
|
|
56
|
+
if (orEqual ? dv < db : dv <= db)
|
|
57
|
+
return new Date(db + 86400_000).toISOString().slice(0, 10);
|
|
58
|
+
}
|
|
59
|
+
else if (orEqual ? dv > db : dv >= db) {
|
|
60
|
+
return new Date(db - 86400_000).toISOString().slice(0, 10);
|
|
61
|
+
}
|
|
62
|
+
return v;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (typeof v === 'number' && typeof b === 'number') {
|
|
66
|
+
if (dir === 'after')
|
|
67
|
+
return orEqual ? Math.max(v, b) : Math.max(v, b + 1);
|
|
68
|
+
return orEqual ? Math.min(v, b) : Math.min(v, b - 1);
|
|
69
|
+
}
|
|
70
|
+
return v;
|
|
71
|
+
}
|
|
72
|
+
function fitStringLength(val, len) {
|
|
73
|
+
if (!len || typeof val !== 'string')
|
|
74
|
+
return val;
|
|
75
|
+
let v = val;
|
|
76
|
+
if (len.exact != null) {
|
|
77
|
+
if (v.length > len.exact)
|
|
78
|
+
v = v.slice(0, len.exact);
|
|
79
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
80
|
+
while (v.length < len.exact) {
|
|
81
|
+
v += chars[Math.floor(Math.random() * chars.length)];
|
|
82
|
+
}
|
|
83
|
+
return v;
|
|
84
|
+
}
|
|
85
|
+
if (len.max != null && v.length > len.max) {
|
|
86
|
+
v = v.slice(0, len.max);
|
|
87
|
+
}
|
|
88
|
+
if (len.min != null && v.length < len.min) {
|
|
89
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
90
|
+
while (v.length < len.min) {
|
|
91
|
+
v += chars[Math.floor(Math.random() * chars.length)];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return v;
|
|
95
|
+
}
|
|
96
|
+
function applyOrdering(fields, ordering) {
|
|
97
|
+
if (!ordering.size)
|
|
98
|
+
return;
|
|
99
|
+
const byCol = new Map(fields.map((f) => [f.column, f]));
|
|
100
|
+
for (const f of fields) {
|
|
101
|
+
const rel = ordering.get(f.column);
|
|
102
|
+
if (!rel)
|
|
103
|
+
continue;
|
|
104
|
+
if (rel.after && f.value != null) {
|
|
105
|
+
const other = byCol.get(rel.after.column);
|
|
106
|
+
if (other && other.value != null)
|
|
107
|
+
f.value = orderedValue(f.value, other.value, 'after', rel.after.orEqual);
|
|
108
|
+
}
|
|
109
|
+
if (rel.before && f.value != null) {
|
|
110
|
+
const other = byCol.get(rel.before.column);
|
|
111
|
+
if (other && other.value != null)
|
|
112
|
+
f.value = orderedValue(f.value, other.value, 'before', rel.before.orEqual);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function buildErGraph(tables) {
|
|
117
|
+
const graph = new Map();
|
|
118
|
+
for (const info of tables) {
|
|
119
|
+
const columns = (0, detector_1.buildColumnConfigs)(info);
|
|
120
|
+
const colMap = new Map(info.columns.map((c) => [c.name, c]));
|
|
121
|
+
const parents = [];
|
|
122
|
+
let hasSelfRef = false;
|
|
123
|
+
for (const fk of info.foreignKeys) {
|
|
124
|
+
const toCol = fk.to || (tables.find((t) => t.table === fk.table)?.primaryKey[0] ?? 'id');
|
|
125
|
+
if (fk.table === info.table) {
|
|
126
|
+
hasSelfRef = true;
|
|
127
|
+
}
|
|
128
|
+
const c = colMap.get(fk.from);
|
|
129
|
+
const notnull = Boolean(c?.notnull && !c?.pk);
|
|
130
|
+
parents.push({ table: fk.table, from: fk.from, to: toCol, notnull });
|
|
131
|
+
}
|
|
132
|
+
graph.set(info.table, {
|
|
133
|
+
name: info.table,
|
|
134
|
+
info,
|
|
135
|
+
columns,
|
|
136
|
+
parents,
|
|
137
|
+
children: [],
|
|
138
|
+
hasSelfRef,
|
|
139
|
+
depth: 0,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
for (const node of graph.values()) {
|
|
143
|
+
for (const p of node.parents) {
|
|
144
|
+
if (p.table !== node.name && graph.has(p.table)) {
|
|
145
|
+
graph.get(p.table).children.push({ table: node.name, from: p.from, to: p.to, notnull: p.notnull });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return graph;
|
|
150
|
+
}
|
|
151
|
+
function resolveErChain(graph, rootTable, scope = 'chain') {
|
|
152
|
+
if (scope === 'all') {
|
|
153
|
+
return Array.from(graph.values());
|
|
154
|
+
}
|
|
155
|
+
const root = graph.get(rootTable);
|
|
156
|
+
if (!root)
|
|
157
|
+
return [];
|
|
158
|
+
if (scope === 'single') {
|
|
159
|
+
return [root];
|
|
160
|
+
}
|
|
161
|
+
const selected = new Set();
|
|
162
|
+
if (scope === 'ancestors') {
|
|
163
|
+
const queue = [rootTable];
|
|
164
|
+
selected.add(rootTable);
|
|
165
|
+
while (queue.length > 0) {
|
|
166
|
+
const current = queue.shift();
|
|
167
|
+
const node = graph.get(current);
|
|
168
|
+
if (!node)
|
|
169
|
+
continue;
|
|
170
|
+
for (const p of node.parents) {
|
|
171
|
+
if (p.table !== current && graph.has(p.table) && !selected.has(p.table)) {
|
|
172
|
+
selected.add(p.table);
|
|
173
|
+
queue.push(p.table);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (scope === 'descendants') {
|
|
179
|
+
const queue = [rootTable];
|
|
180
|
+
selected.add(rootTable);
|
|
181
|
+
while (queue.length > 0) {
|
|
182
|
+
const current = queue.shift();
|
|
183
|
+
const node = graph.get(current);
|
|
184
|
+
if (!node)
|
|
185
|
+
continue;
|
|
186
|
+
for (const c of node.children) {
|
|
187
|
+
if (c.table !== current && graph.has(c.table) && !selected.has(c.table)) {
|
|
188
|
+
selected.add(c.table);
|
|
189
|
+
queue.push(c.table);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const queue = [rootTable];
|
|
196
|
+
selected.add(rootTable);
|
|
197
|
+
while (queue.length > 0) {
|
|
198
|
+
const current = queue.shift();
|
|
199
|
+
const node = graph.get(current);
|
|
200
|
+
if (!node)
|
|
201
|
+
continue;
|
|
202
|
+
for (const p of node.parents) {
|
|
203
|
+
if (p.table !== current && graph.has(p.table) && !selected.has(p.table)) {
|
|
204
|
+
selected.add(p.table);
|
|
205
|
+
queue.push(p.table);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const c of node.children) {
|
|
209
|
+
if (c.table !== current && graph.has(c.table) && !selected.has(c.table)) {
|
|
210
|
+
selected.add(c.table);
|
|
211
|
+
queue.push(c.table);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return Array.from(selected).map((name) => graph.get(name));
|
|
217
|
+
}
|
|
218
|
+
function topologicalSort(nodes) {
|
|
219
|
+
const activeNames = new Set(nodes.map((n) => n.name));
|
|
220
|
+
const nodeMap = new Map(nodes.map((n) => [n.name, n]));
|
|
221
|
+
const hardInDegree = new Map();
|
|
222
|
+
const totalInDegree = new Map();
|
|
223
|
+
const hardForwardEdges = new Map();
|
|
224
|
+
const softForwardEdges = new Map();
|
|
225
|
+
for (const n of nodes) {
|
|
226
|
+
hardInDegree.set(n.name, 0);
|
|
227
|
+
totalInDegree.set(n.name, 0);
|
|
228
|
+
hardForwardEdges.set(n.name, new Set());
|
|
229
|
+
softForwardEdges.set(n.name, new Set());
|
|
230
|
+
}
|
|
231
|
+
for (const n of nodes) {
|
|
232
|
+
for (const p of n.parents) {
|
|
233
|
+
if (p.table !== n.name && activeNames.has(p.table)) {
|
|
234
|
+
if (p.notnull) {
|
|
235
|
+
if (!hardForwardEdges.get(p.table).has(n.name)) {
|
|
236
|
+
hardForwardEdges.get(p.table).add(n.name);
|
|
237
|
+
hardInDegree.set(n.name, (hardInDegree.get(n.name) ?? 0) + 1);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
if (!softForwardEdges.get(p.table).has(n.name)) {
|
|
242
|
+
softForwardEdges.get(p.table).add(n.name);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
totalInDegree.set(n.name, (totalInDegree.get(n.name) ?? 0) + 1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const queue = [];
|
|
250
|
+
const visited = new Set();
|
|
251
|
+
for (const [name, deg] of hardInDegree.entries()) {
|
|
252
|
+
if (deg === 0) {
|
|
253
|
+
queue.push(name);
|
|
254
|
+
visited.add(name);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const sortedNames = [];
|
|
258
|
+
const depths = new Map();
|
|
259
|
+
for (const name of queue)
|
|
260
|
+
depths.set(name, 0);
|
|
261
|
+
let cycleDetected = false;
|
|
262
|
+
const cycleTables = [];
|
|
263
|
+
while (visited.size < nodes.length) {
|
|
264
|
+
while (queue.length > 0) {
|
|
265
|
+
const curr = queue.shift();
|
|
266
|
+
sortedNames.push(curr);
|
|
267
|
+
const currDepth = depths.get(curr) ?? 0;
|
|
268
|
+
for (const child of hardForwardEdges.get(curr) ?? []) {
|
|
269
|
+
const newDeg = (hardInDegree.get(child) ?? 1) - 1;
|
|
270
|
+
hardInDegree.set(child, newDeg);
|
|
271
|
+
depths.set(child, Math.max(depths.get(child) ?? 0, currDepth + 1));
|
|
272
|
+
if (newDeg === 0 && !visited.has(child)) {
|
|
273
|
+
visited.add(child);
|
|
274
|
+
queue.push(child);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
for (const child of softForwardEdges.get(curr) ?? []) {
|
|
278
|
+
depths.set(child, Math.max(depths.get(child) ?? 0, currDepth + 1));
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (visited.size < nodes.length) {
|
|
282
|
+
cycleDetected = true;
|
|
283
|
+
let bestCandidate = '';
|
|
284
|
+
let lowestDeg = Infinity;
|
|
285
|
+
for (const n of nodes) {
|
|
286
|
+
if (!visited.has(n.name)) {
|
|
287
|
+
const deg = hardInDegree.get(n.name) ?? 0;
|
|
288
|
+
if (deg < lowestDeg) {
|
|
289
|
+
lowestDeg = deg;
|
|
290
|
+
bestCandidate = n.name;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (bestCandidate) {
|
|
295
|
+
cycleTables.push(bestCandidate);
|
|
296
|
+
visited.add(bestCandidate);
|
|
297
|
+
depths.set(bestCandidate, (depths.get(bestCandidate) ?? 1) + 1);
|
|
298
|
+
queue.push(bestCandidate);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const sorted = sortedNames.map((name) => {
|
|
306
|
+
const node = { ...nodeMap.get(name) };
|
|
307
|
+
node.depth = depths.get(name) ?? 0;
|
|
308
|
+
return node;
|
|
309
|
+
});
|
|
310
|
+
return { sorted, cycleDetected, cycleTables };
|
|
311
|
+
}
|
|
312
|
+
async function getErChainConfig(db, rootTable, scope = 'chain') {
|
|
313
|
+
const tableListRes = await db.listTables();
|
|
314
|
+
const tableNames = (tableListRes.data ?? [])
|
|
315
|
+
.map((t) => t.name)
|
|
316
|
+
.filter((name) => !name.startsWith('sqlite_') && name !== '_saved_queries');
|
|
317
|
+
const tables = [];
|
|
318
|
+
for (const name of tableNames) {
|
|
319
|
+
const info = await db.getTableInfo(name);
|
|
320
|
+
if (info.success && info.data)
|
|
321
|
+
tables.push(info.data);
|
|
322
|
+
}
|
|
323
|
+
const graph = buildErGraph(tables);
|
|
324
|
+
const chainNodes = resolveErChain(graph, rootTable, scope);
|
|
325
|
+
const { sorted, cycleDetected, cycleTables } = topologicalSort(chainNodes);
|
|
326
|
+
const tableNodes = [];
|
|
327
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
328
|
+
const node = sorted[i];
|
|
329
|
+
let rowCount = 0;
|
|
330
|
+
try {
|
|
331
|
+
const countRes = await db.getRowCount(node.name);
|
|
332
|
+
if (countRes.success && typeof countRes.data === 'number')
|
|
333
|
+
rowCount = countRes.data;
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
}
|
|
337
|
+
let suggestedCount = 10;
|
|
338
|
+
if (node.depth === 0)
|
|
339
|
+
suggestedCount = 5;
|
|
340
|
+
else if (node.depth === 1)
|
|
341
|
+
suggestedCount = 15;
|
|
342
|
+
else
|
|
343
|
+
suggestedCount = 25;
|
|
344
|
+
tableNodes.push({
|
|
345
|
+
name: node.name,
|
|
346
|
+
order: i + 1,
|
|
347
|
+
depth: node.depth,
|
|
348
|
+
parents: node.parents.filter((p) => sorted.some((s) => s.name === p.table)),
|
|
349
|
+
children: node.children.filter((c) => sorted.some((s) => s.name === c.table)),
|
|
350
|
+
columns: node.columns,
|
|
351
|
+
suggestedCount,
|
|
352
|
+
rowCount,
|
|
353
|
+
isRoot: node.name === rootTable,
|
|
354
|
+
hasSelfRef: node.hasSelfRef,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return {
|
|
358
|
+
rootTable,
|
|
359
|
+
scope,
|
|
360
|
+
tables: tableNodes,
|
|
361
|
+
cycleDetected,
|
|
362
|
+
cycleTables,
|
|
363
|
+
maxTotalRows: 10000,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async function generateChainRows(db, chainConfig, customPlans = {}, customCounts = {}, truncate = false) {
|
|
367
|
+
const warnings = [];
|
|
368
|
+
const executionOrder = chainConfig.tables.map((t) => t.name);
|
|
369
|
+
const relationalPool = new Map();
|
|
370
|
+
const tableResults = {};
|
|
371
|
+
let totalRows = 0;
|
|
372
|
+
for (const tableNode of chainConfig.tables) {
|
|
373
|
+
const tableName = tableNode.name;
|
|
374
|
+
const tableInfoRes = await db.getTableInfo(tableName);
|
|
375
|
+
if (!tableInfoRes.success || !tableInfoRes.data) {
|
|
376
|
+
warnings.push(`Could not load table metadata for "${tableName}".`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const info = tableInfoRes.data;
|
|
380
|
+
let count = Math.max(1, Math.min(2000, customCounts[tableName] ?? tableNode.suggestedCount ?? 10));
|
|
381
|
+
const tablePlan = customPlans[tableName] ?? {};
|
|
382
|
+
const configs = tableNode.columns;
|
|
383
|
+
const fkByColumn = new Map();
|
|
384
|
+
for (const fk of info.foreignKeys)
|
|
385
|
+
fkByColumn.set(fk.from, fk);
|
|
386
|
+
const uniqueCols = new Set();
|
|
387
|
+
for (const cfg of configs)
|
|
388
|
+
if (cfg.unique)
|
|
389
|
+
uniqueCols.add(cfg.name);
|
|
390
|
+
const compositeUniques = [];
|
|
391
|
+
for (const ix of info.indexes ?? []) {
|
|
392
|
+
if (ix.unique && ix.columns.length === 1 && ix.columns[0])
|
|
393
|
+
uniqueCols.add(ix.columns[0]);
|
|
394
|
+
if (ix.unique && ix.columns.length > 1)
|
|
395
|
+
compositeUniques.push(ix.columns);
|
|
396
|
+
}
|
|
397
|
+
if (info.sql) {
|
|
398
|
+
const uqRe = /UNIQUE\s*\(\s*([^)]+)\s*\)/gi;
|
|
399
|
+
let uqM;
|
|
400
|
+
while ((uqM = uqRe.exec(info.sql))) {
|
|
401
|
+
const rawCols = uqM[1].split(',').map((s) => s.trim().replace(/[`"\[\]]/g, ''));
|
|
402
|
+
if (rawCols.length === 1 && rawCols[0]) {
|
|
403
|
+
uniqueCols.add(rawCols[0]);
|
|
404
|
+
}
|
|
405
|
+
else if (rawCols.length > 1 && !compositeUniques.some((cu) => cu.join(',') === rawCols.join(','))) {
|
|
406
|
+
compositeUniques.push(rawCols);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const compositeSeen = new Map();
|
|
411
|
+
for (const cu of compositeUniques) {
|
|
412
|
+
compositeSeen.set(cu.join('::'), new Set());
|
|
413
|
+
}
|
|
414
|
+
const checkInfo = (0, detector_1.parseChecks)(info.sql ?? null);
|
|
415
|
+
const prepared = [];
|
|
416
|
+
const fkPools = new Map();
|
|
417
|
+
const fkOmit = new Set();
|
|
418
|
+
for (const col of info.columns) {
|
|
419
|
+
const cfg = configs.find((c) => c.name === col.name);
|
|
420
|
+
const raw = tablePlan[col.name];
|
|
421
|
+
const p = raw && raw.strategy ? raw : (cfg?.defaultPlan ?? { strategy: 'skip' });
|
|
422
|
+
if (p.strategy === 'list' && (!p.values || !p.values.length)) {
|
|
423
|
+
const allowed = (0, detector_1.findInMap)(checkInfo.columnAllowed, col.name);
|
|
424
|
+
if (allowed && allowed.length)
|
|
425
|
+
p.values = [...allowed];
|
|
426
|
+
}
|
|
427
|
+
if (p.strategy === 'fk') {
|
|
428
|
+
const fk = fkByColumn.get(col.name);
|
|
429
|
+
const refTable = fk?.table ?? '';
|
|
430
|
+
const refCol = fk?.to || 'id';
|
|
431
|
+
const poolKey = `${refTable}.${refCol}`;
|
|
432
|
+
let pool = relationalPool.get(poolKey) ?? [];
|
|
433
|
+
if (!pool.length) {
|
|
434
|
+
for (const [k, v] of relationalPool.entries()) {
|
|
435
|
+
if (k.toLowerCase() === poolKey.toLowerCase() && v.length) {
|
|
436
|
+
pool = v;
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (!pool.length) {
|
|
442
|
+
pool = await sampleFkValues(db, fk ?? null);
|
|
443
|
+
}
|
|
444
|
+
fkPools.set(col.name, pool);
|
|
445
|
+
if (!pool.length) {
|
|
446
|
+
if (col.notnull && !col.pk) {
|
|
447
|
+
pool = [1];
|
|
448
|
+
fkPools.set(col.name, pool);
|
|
449
|
+
warnings.push(`Table "${tableName}" Column "${col.name}" is a NOT NULL foreign key to "${refTable}". Fallback ID 1 was used.`);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
warnings.push(`Table "${tableName}" Column "${col.name}": "${refTable}" has no rows; NULL will be used.`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
prepared.push({ col, plan: p });
|
|
457
|
+
}
|
|
458
|
+
const rows = [];
|
|
459
|
+
const previewRows = [];
|
|
460
|
+
const uniqueSeen = new Map();
|
|
461
|
+
if (!truncate) {
|
|
462
|
+
for (const col of uniqueCols) {
|
|
463
|
+
try {
|
|
464
|
+
const existing = await db.all(`SELECT ${(0, generator_1.quoteIdentifier)(col)} AS v FROM ${(0, generator_1.quoteIdentifier)(tableName)} WHERE ${(0, generator_1.quoteIdentifier)(col)} IS NOT NULL LIMIT 10000`);
|
|
465
|
+
const seen = new Set();
|
|
466
|
+
for (const r of (existing.data ?? [])) {
|
|
467
|
+
if (r.v != null)
|
|
468
|
+
seen.add(stringifyKey(r.v));
|
|
469
|
+
}
|
|
470
|
+
uniqueSeen.set(col, seen);
|
|
471
|
+
}
|
|
472
|
+
catch { }
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
const tableGeneratedPkVals = {};
|
|
476
|
+
let pkStartOffset = 0;
|
|
477
|
+
for (const col of info.columns) {
|
|
478
|
+
if (col.pk && (col.type || '').toUpperCase().includes('INT')) {
|
|
479
|
+
try {
|
|
480
|
+
const maxRes = await db.all(`SELECT MAX(${(0, generator_1.quoteIdentifier)(col.name)}) AS mx FROM ${(0, generator_1.quoteIdentifier)(tableName)}`);
|
|
481
|
+
const maxVal = (maxRes.data ?? [])[0]?.mx;
|
|
482
|
+
if (typeof maxVal === 'number' && Number.isFinite(maxVal))
|
|
483
|
+
pkStartOffset = maxVal;
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
const isUniqueCol = (name) => {
|
|
490
|
+
const norm = name.toLowerCase().replace(/[`"\[\]]/g, '');
|
|
491
|
+
for (const u of uniqueCols) {
|
|
492
|
+
if (u.toLowerCase().replace(/[`"\[\]]/g, '') === norm)
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
return false;
|
|
496
|
+
};
|
|
497
|
+
for (const { col, plan: p } of prepared) {
|
|
498
|
+
if (p.strategy === 'fk' && (col.pk || isUniqueCol(col.name))) {
|
|
499
|
+
const pool = fkPools.get(col.name);
|
|
500
|
+
if (pool && pool.length > 0 && pool.length < count) {
|
|
501
|
+
count = pool.length;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (compositeUniques.length > 0) {
|
|
506
|
+
for (const cu of compositeUniques) {
|
|
507
|
+
const isAllFk = cu.every((colName) => prepared.some((p) => (p.col.name === colName || p.col.name.replace(/[`"\[\]]/g, '') === colName.replace(/[`"\[\]]/g, '')) && p.plan.strategy === 'fk'));
|
|
508
|
+
if (isAllFk) {
|
|
509
|
+
const maxCombos = cu.reduce((acc, colName) => {
|
|
510
|
+
const pool = fkPools.get(colName) ?? [];
|
|
511
|
+
return acc * Math.max(1, pool.length);
|
|
512
|
+
}, 1);
|
|
513
|
+
if (maxCombos > 0 && maxCombos < count) {
|
|
514
|
+
count = maxCombos;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
for (let i = 0; i < count; i++) {
|
|
520
|
+
let fields = [];
|
|
521
|
+
let previewRow = {};
|
|
522
|
+
let rowValid = false;
|
|
523
|
+
let attempt = 0;
|
|
524
|
+
while (!rowValid && attempt < 30) {
|
|
525
|
+
fields = [];
|
|
526
|
+
previewRow = {};
|
|
527
|
+
for (const { col, plan: p } of prepared) {
|
|
528
|
+
if (p.strategy === 'fk' && fkOmit.has(col.name)) {
|
|
529
|
+
previewRow[col.name] = null;
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
const isSelfFk = fkByColumn.get(col.name)?.table === tableName;
|
|
533
|
+
let pool = fkPools.get(col.name);
|
|
534
|
+
if (isSelfFk) {
|
|
535
|
+
const existingSelfIds = tableGeneratedPkVals[fkByColumn.get(col.name)?.to || info.primaryKey[0] || 'id'] ?? [];
|
|
536
|
+
if (i === 0 || existingSelfIds.length === 0 || (Math.random() < 0.35 && !col.notnull)) {
|
|
537
|
+
pool = [];
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
pool = existingSelfIds;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
let v = (0, strategies_1.generateOne)(col, p, pool, i + attempt * 50);
|
|
544
|
+
if (v === types_1.SKIP && col.pk && (col.type || '').toUpperCase().includes('INT')) {
|
|
545
|
+
v = pkStartOffset + i + 1;
|
|
546
|
+
}
|
|
547
|
+
if (v === types_1.SKIP) {
|
|
548
|
+
previewRow[col.name] = '(default)';
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
if (isUniqueCol(col.name)) {
|
|
552
|
+
const seen = uniqueSeen.get(col.name) ?? new Set();
|
|
553
|
+
if (p.strategy === 'fk') {
|
|
554
|
+
const unused = (pool ?? []).filter((x) => !seen.has(stringifyKey(x)));
|
|
555
|
+
if (unused.length > 0) {
|
|
556
|
+
v = unused[0];
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
rowValid = false;
|
|
560
|
+
break;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
let singleAtt = 0;
|
|
565
|
+
while (seen.has(stringifyKey(v)) && singleAtt < 100) {
|
|
566
|
+
v = (0, strategies_1.generateOne)(col, p, pool, i + attempt * 50 + singleAtt * 100);
|
|
567
|
+
singleAtt += 1;
|
|
568
|
+
}
|
|
569
|
+
if (seen.has(stringifyKey(v))) {
|
|
570
|
+
rowValid = false;
|
|
571
|
+
break;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
v = fitStringLength(v, (0, detector_1.findInMap)(checkInfo.columnLength, col.name));
|
|
576
|
+
fields.push({ column: col.name, value: v });
|
|
577
|
+
previewRow[col.name] = v;
|
|
578
|
+
}
|
|
579
|
+
applyOrdering(fields, checkInfo.ordering);
|
|
580
|
+
for (const f of fields) {
|
|
581
|
+
previewRow[f.column] = f.value;
|
|
582
|
+
}
|
|
583
|
+
let compositeCollision = false;
|
|
584
|
+
if (compositeUniques.length > 0) {
|
|
585
|
+
const fieldMap = new Map(fields.map((f) => [f.column, f.value]));
|
|
586
|
+
for (const cu of compositeUniques) {
|
|
587
|
+
const key = cu.join('::');
|
|
588
|
+
const compositeVal = cu.map((colName) => stringifyKey(fieldMap.get(colName))).join('|');
|
|
589
|
+
const seenSet = compositeSeen.get(key) ?? new Set();
|
|
590
|
+
if (seenSet.has(compositeVal)) {
|
|
591
|
+
compositeCollision = true;
|
|
592
|
+
break;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (!compositeCollision) {
|
|
597
|
+
rowValid = true;
|
|
598
|
+
if (compositeUniques.length > 0) {
|
|
599
|
+
const fieldMap = new Map(fields.map((f) => [f.column, f.value]));
|
|
600
|
+
for (const cu of compositeUniques) {
|
|
601
|
+
const key = cu.join('::');
|
|
602
|
+
const compositeVal = cu.map((colName) => stringifyKey(fieldMap.get(colName))).join('|');
|
|
603
|
+
const seenSet = compositeSeen.get(key) ?? new Set();
|
|
604
|
+
seenSet.add(compositeVal);
|
|
605
|
+
compositeSeen.set(key, seenSet);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
attempt += 1;
|
|
610
|
+
}
|
|
611
|
+
if (!rowValid) {
|
|
612
|
+
break;
|
|
613
|
+
}
|
|
614
|
+
for (const f of fields) {
|
|
615
|
+
if (isUniqueCol(f.column)) {
|
|
616
|
+
const seen = uniqueSeen.get(f.column) ?? new Set();
|
|
617
|
+
seen.add(stringifyKey(f.value));
|
|
618
|
+
uniqueSeen.set(f.column, seen);
|
|
619
|
+
}
|
|
620
|
+
const poolKey = `${tableName}.${f.column}`;
|
|
621
|
+
const cur = relationalPool.get(poolKey) ?? [];
|
|
622
|
+
cur.push(f.value);
|
|
623
|
+
relationalPool.set(poolKey, cur);
|
|
624
|
+
const selfCur = tableGeneratedPkVals[f.column] ?? [];
|
|
625
|
+
selfCur.push(f.value);
|
|
626
|
+
tableGeneratedPkVals[f.column] = selfCur;
|
|
627
|
+
}
|
|
628
|
+
rows.push(fields);
|
|
629
|
+
if (previewRows.length < 50) {
|
|
630
|
+
previewRows.push(previewRow);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
tableResults[tableName] = {
|
|
634
|
+
count: rows.length,
|
|
635
|
+
rows,
|
|
636
|
+
previewRows,
|
|
637
|
+
warnings,
|
|
638
|
+
};
|
|
639
|
+
totalRows += rows.length;
|
|
640
|
+
}
|
|
641
|
+
const sql = buildChainInsertSql(executionOrder, tableResults);
|
|
642
|
+
return {
|
|
643
|
+
rootTable: chainConfig.rootTable,
|
|
644
|
+
scope: chainConfig.scope,
|
|
645
|
+
executionOrder,
|
|
646
|
+
tableResults,
|
|
647
|
+
totalRows,
|
|
648
|
+
sql,
|
|
649
|
+
warnings,
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
function buildChainInsertSql(executionOrder, tableResults) {
|
|
653
|
+
const sqlParts = ['-- AdminDB ER Chain Seed Script', '-- Order of execution: ' + executionOrder.join(' -> '), 'BEGIN TRANSACTION;\n'];
|
|
654
|
+
for (const table of executionOrder) {
|
|
655
|
+
const res = tableResults[table];
|
|
656
|
+
if (!res || !res.rows.length)
|
|
657
|
+
continue;
|
|
658
|
+
sqlParts.push(`-- --------------------------------------------------------`);
|
|
659
|
+
sqlParts.push(`-- Table: ${table} (${res.rows.length} rows)`);
|
|
660
|
+
sqlParts.push(`-- --------------------------------------------------------`);
|
|
661
|
+
for (const fields of res.rows) {
|
|
662
|
+
if (!fields.length)
|
|
663
|
+
continue;
|
|
664
|
+
const cols = fields.map((f) => (0, generator_1.quoteIdentifier)(f.column)).join(', ');
|
|
665
|
+
const vals = fields.map((f) => (0, generator_1.sqlValue)(f.value)).join(', ');
|
|
666
|
+
sqlParts.push(`INSERT INTO ${(0, generator_1.quoteIdentifier)(table)} (${cols}) VALUES (${vals});`);
|
|
667
|
+
}
|
|
668
|
+
sqlParts.push('');
|
|
669
|
+
}
|
|
670
|
+
sqlParts.push('COMMIT;');
|
|
671
|
+
return sqlParts.join('\n');
|
|
672
|
+
}
|
|
673
|
+
async function executeChainInsert(db, chainResult, truncate = false) {
|
|
674
|
+
const t0 = Date.now();
|
|
675
|
+
const inserted = {};
|
|
676
|
+
let totalInserted = 0;
|
|
677
|
+
if (truncate) {
|
|
678
|
+
const reverseOrder = [...chainResult.executionOrder].reverse();
|
|
679
|
+
for (const table of reverseOrder) {
|
|
680
|
+
const clearRes = await db.run(`DELETE FROM ${(0, generator_1.quoteIdentifier)(table)}`);
|
|
681
|
+
if (!clearRes.success) {
|
|
682
|
+
throw new Error(`Failed to truncate table "${table}": ${clearRes.error}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
for (const table of chainResult.executionOrder) {
|
|
687
|
+
const res = chainResult.tableResults[table];
|
|
688
|
+
if (!res || !res.rows.length) {
|
|
689
|
+
inserted[table] = 0;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const insertRes = await db.insertRows(table, res.rows);
|
|
693
|
+
if (!insertRes.success) {
|
|
694
|
+
throw new Error(`Failed to insert seed rows into "${table}": ${insertRes.error}`);
|
|
695
|
+
}
|
|
696
|
+
const count = insertRes.data?.inserted ?? res.rows.length;
|
|
697
|
+
inserted[table] = count;
|
|
698
|
+
totalInserted += count;
|
|
699
|
+
}
|
|
700
|
+
const elapsedMs = Date.now() - t0;
|
|
701
|
+
return {
|
|
702
|
+
inserted,
|
|
703
|
+
totalInserted,
|
|
704
|
+
elapsedMs,
|
|
705
|
+
warnings: chainResult.warnings,
|
|
706
|
+
};
|
|
707
|
+
}
|