@zintrust/d1-migrator 1.9.2 → 1.9.3
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.
|
@@ -19,11 +19,18 @@ export interface TargetConnection {
|
|
|
19
19
|
database: string;
|
|
20
20
|
connected: boolean;
|
|
21
21
|
adapter?: DatabaseAdapter;
|
|
22
|
+
remoteBatchTuning?: RemoteBatchTuning;
|
|
22
23
|
}
|
|
23
24
|
export interface TableInfo {
|
|
24
25
|
name: string;
|
|
25
26
|
rowCount?: number;
|
|
27
|
+
dependsOn?: string[];
|
|
26
28
|
}
|
|
29
|
+
type RemoteBatchTuning = {
|
|
30
|
+
rowsPerStatement: number;
|
|
31
|
+
maxStatementSqlLength: number;
|
|
32
|
+
maxExecutionSqlLength: number;
|
|
33
|
+
};
|
|
27
34
|
type AdapterQueryResult = {
|
|
28
35
|
rows: Record<string, unknown>[];
|
|
29
36
|
rowCount?: number;
|
|
@@ -77,6 +84,7 @@ export declare const DataMigrator: Readonly<{
|
|
|
77
84
|
rowsMigrated: number;
|
|
78
85
|
errors: string[];
|
|
79
86
|
}>;
|
|
87
|
+
processTableChunks(table: TableInfo, sourceConnection: SourceConnection, targetConnection: TargetConnection, totalRows: number, batchSize: number, startOffset: number, errors: string[]): Promise<number>;
|
|
80
88
|
/**
|
|
81
89
|
* Read data chunk from source database
|
|
82
90
|
*/
|
|
@@ -84,7 +92,7 @@ export declare const DataMigrator: Readonly<{
|
|
|
84
92
|
/**
|
|
85
93
|
* Transform data for D1 compatibility
|
|
86
94
|
*/
|
|
87
|
-
transformData(chunk: Record<string, unknown>[],
|
|
95
|
+
transformData(chunk: Record<string, unknown>[], _tableName: string): Promise<Record<string, unknown>[]>;
|
|
88
96
|
/**
|
|
89
97
|
* Insert data into target database
|
|
90
98
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DataMigrator.d.ts","sourceRoot":"","sources":["../../src/cli/DataMigrator.ts"],"names":[],"mappings":"AACA;;;GAGG;AAWH,OAAO,KAAK,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAEnE;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,CAAC;IACxC,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,eAAe,CAAC,wBAAwB,CAAC,CAAC;IACnE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,eAAe,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,IAAI,GAAG,WAAW,CAAC;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"DataMigrator.d.ts","sourceRoot":"","sources":["../../src/cli/DataMigrator.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/DataMigrator.js
CHANGED
|
@@ -22,6 +22,35 @@ const extractWranglerJson = (output) => {
|
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
|
+
const normalizeWranglerTableValue = (value) => {
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (trimmed === '') {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
30
|
+
const normalized = trimmed.toLowerCase();
|
|
31
|
+
if (normalized === 'null') {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
if (normalized === 'true') {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (normalized === 'false') {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (/^-?\d+$/.test(trimmed)) {
|
|
41
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
42
|
+
if (Number.isSafeInteger(parsed)) {
|
|
43
|
+
return parsed;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (/^-?(?:\d+\.\d+|\d+\.\d*|\.\d+)$/.test(trimmed)) {
|
|
47
|
+
const parsed = Number.parseFloat(trimmed);
|
|
48
|
+
if (Number.isFinite(parsed)) {
|
|
49
|
+
return parsed;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return trimmed;
|
|
53
|
+
};
|
|
25
54
|
const parseWranglerTable = (output) => {
|
|
26
55
|
const lines = output.split('\n').map((line) => line.trim());
|
|
27
56
|
const dataLines = lines.filter((line) => line.startsWith('│') && line.endsWith('│'));
|
|
@@ -49,6 +78,15 @@ const parseWranglerTable = (output) => {
|
|
|
49
78
|
}
|
|
50
79
|
return rows;
|
|
51
80
|
};
|
|
81
|
+
const parseWranglerTableRows = (output) => {
|
|
82
|
+
return parseWranglerTable(output).map((row) => {
|
|
83
|
+
const normalizedRow = {};
|
|
84
|
+
for (const [key, value] of Object.entries(row)) {
|
|
85
|
+
normalizedRow[key] = normalizeWranglerTableValue(value);
|
|
86
|
+
}
|
|
87
|
+
return normalizedRow;
|
|
88
|
+
});
|
|
89
|
+
};
|
|
52
90
|
const toHex = (value) => {
|
|
53
91
|
return Array.from(value)
|
|
54
92
|
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
@@ -94,6 +132,228 @@ const bindSqlParameters = (sql, parameters) => {
|
|
|
94
132
|
return rendered;
|
|
95
133
|
});
|
|
96
134
|
};
|
|
135
|
+
const REMOTE_INSERT_ROWS_PER_STATEMENT = 200;
|
|
136
|
+
const LOCAL_INSERT_ROWS_PER_STATEMENT = 500;
|
|
137
|
+
const MAX_REMOTE_INSERT_SQL_LENGTH = 100000;
|
|
138
|
+
const MAX_REMOTE_EXECUTION_SQL_LENGTH = 250000;
|
|
139
|
+
const MIN_REMOTE_INSERT_ROWS_PER_STATEMENT = 50;
|
|
140
|
+
const MAX_REMOTE_INSERT_ROWS_PER_STATEMENT = 2000;
|
|
141
|
+
const DEFAULT_REMOTE_TABLE_PARALLELISM = 4;
|
|
142
|
+
const formatDuration = (durationMs) => {
|
|
143
|
+
if (durationMs < 1000) {
|
|
144
|
+
return `${durationMs}ms`;
|
|
145
|
+
}
|
|
146
|
+
return `${(durationMs / 1000).toFixed(durationMs < 10000 ? 2 : 1)}s`;
|
|
147
|
+
};
|
|
148
|
+
const formatRowsPerSecond = (rows, durationMs) => {
|
|
149
|
+
if (rows <= 0 || durationMs <= 0) {
|
|
150
|
+
return 'n/a';
|
|
151
|
+
}
|
|
152
|
+
const rate = rows / (durationMs / 1000);
|
|
153
|
+
return `${rate >= 100 ? rate.toFixed(0) : rate.toFixed(2)} rows/s`;
|
|
154
|
+
};
|
|
155
|
+
const getRemoteBatchTuning = (connection) => {
|
|
156
|
+
if (connection.remoteBatchTuning !== undefined) {
|
|
157
|
+
return connection.remoteBatchTuning;
|
|
158
|
+
}
|
|
159
|
+
connection.remoteBatchTuning = {
|
|
160
|
+
rowsPerStatement: REMOTE_INSERT_ROWS_PER_STATEMENT,
|
|
161
|
+
maxStatementSqlLength: MAX_REMOTE_INSERT_SQL_LENGTH,
|
|
162
|
+
maxExecutionSqlLength: MAX_REMOTE_EXECUTION_SQL_LENGTH,
|
|
163
|
+
};
|
|
164
|
+
return connection.remoteBatchTuning;
|
|
165
|
+
};
|
|
166
|
+
const getInsertBatchSettings = (connection) => {
|
|
167
|
+
if (connection.type === 'd1-remote') {
|
|
168
|
+
const tuning = getRemoteBatchTuning(connection);
|
|
169
|
+
return {
|
|
170
|
+
rowsPerStatement: tuning.rowsPerStatement,
|
|
171
|
+
maxStatementSqlLength: tuning.maxStatementSqlLength,
|
|
172
|
+
maxExecutionSqlLength: tuning.maxExecutionSqlLength,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
rowsPerStatement: LOCAL_INSERT_ROWS_PER_STATEMENT,
|
|
177
|
+
maxStatementSqlLength: Number.POSITIVE_INFINITY,
|
|
178
|
+
maxExecutionSqlLength: Number.POSITIVE_INFINITY,
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
const adjustRemoteBatchTuning = (connection, executedRows, durationMs, sqlLength) => {
|
|
182
|
+
if (connection.type !== 'd1-remote' || executedRows <= 0) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const tuning = getRemoteBatchTuning(connection);
|
|
186
|
+
const previousRowsPerStatement = tuning.rowsPerStatement;
|
|
187
|
+
const nearSqlLimit = sqlLength >= Math.floor(tuning.maxExecutionSqlLength * 0.9);
|
|
188
|
+
if (durationMs <= 1500 &&
|
|
189
|
+
executedRows >= Math.floor(previousRowsPerStatement * 0.8) &&
|
|
190
|
+
!nearSqlLimit) {
|
|
191
|
+
tuning.rowsPerStatement = Math.min(MAX_REMOTE_INSERT_ROWS_PER_STATEMENT, Math.max(previousRowsPerStatement + 25, Math.floor(previousRowsPerStatement * 1.25)));
|
|
192
|
+
}
|
|
193
|
+
else if (durationMs >= 6000 || nearSqlLimit) {
|
|
194
|
+
tuning.rowsPerStatement = Math.max(MIN_REMOTE_INSERT_ROWS_PER_STATEMENT, Math.min(previousRowsPerStatement - 25, Math.floor(previousRowsPerStatement * 0.75)));
|
|
195
|
+
}
|
|
196
|
+
if (tuning.rowsPerStatement !== previousRowsPerStatement) {
|
|
197
|
+
Logger.info(`[DataMigrator] Adaptive remote batching: rows_per_statement ${previousRowsPerStatement} -> ${tuning.rowsPerStatement} after ${executedRows} rows in ${formatDuration(durationMs)}`);
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const getTableDependencies = (table) => {
|
|
201
|
+
if (!Array.isArray(table.dependsOn)) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
return [...new Set(table.dependsOn.filter((dependency) => dependency.trim() !== ''))];
|
|
205
|
+
};
|
|
206
|
+
const buildTableMigrationLevels = (tables) => {
|
|
207
|
+
const tablesByName = new Map();
|
|
208
|
+
for (const table of tables) {
|
|
209
|
+
tablesByName.set(table.name, table);
|
|
210
|
+
}
|
|
211
|
+
const unresolved = new Set(tables.map((table) => table.name));
|
|
212
|
+
const dependenciesByTable = new Map();
|
|
213
|
+
for (const table of tables) {
|
|
214
|
+
dependenciesByTable.set(table.name, new Set(getTableDependencies(table).filter((dependency) => dependency !== table.name && tablesByName.has(dependency))));
|
|
215
|
+
}
|
|
216
|
+
const levels = [];
|
|
217
|
+
while (unresolved.size > 0) {
|
|
218
|
+
const readyNames = tables
|
|
219
|
+
.map((table) => table.name)
|
|
220
|
+
.filter((name) => unresolved.has(name) && (dependenciesByTable.get(name)?.size ?? 0) === 0);
|
|
221
|
+
if (readyNames.length === 0) {
|
|
222
|
+
const cyclicTables = tables.filter((table) => unresolved.has(table.name));
|
|
223
|
+
if (cyclicTables.length > 0) {
|
|
224
|
+
Logger.warn(`[DataMigrator] Table dependency cycle or unresolved reference detected. Falling back to sequential execution for: ${cyclicTables.map((table) => table.name).join(', ')}`);
|
|
225
|
+
levels.push(...cyclicTables.map((table) => [table]));
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
const levelTables = readyNames
|
|
230
|
+
.map((name) => tablesByName.get(name))
|
|
231
|
+
.filter((table) => table !== undefined);
|
|
232
|
+
levels.push(levelTables);
|
|
233
|
+
for (const readyName of readyNames) {
|
|
234
|
+
unresolved.delete(readyName);
|
|
235
|
+
}
|
|
236
|
+
for (const name of unresolved) {
|
|
237
|
+
const dependencies = dependenciesByTable.get(name);
|
|
238
|
+
for (const readyName of readyNames) {
|
|
239
|
+
dependencies?.delete(readyName);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return levels;
|
|
244
|
+
};
|
|
245
|
+
const getTableParallelism = (config, targetConnection) => {
|
|
246
|
+
if (targetConnection.type !== 'd1-remote') {
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
if (config.sourceDriver === 'sqlite') {
|
|
250
|
+
return 1;
|
|
251
|
+
}
|
|
252
|
+
return DEFAULT_REMOTE_TABLE_PARALLELISM;
|
|
253
|
+
};
|
|
254
|
+
const executeWithConcurrency = async (items, concurrency, worker) => {
|
|
255
|
+
if (items.length === 0) {
|
|
256
|
+
return [];
|
|
257
|
+
}
|
|
258
|
+
const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
|
|
259
|
+
const results = new Array(items.length);
|
|
260
|
+
let index = 0;
|
|
261
|
+
const runWorker = async () => {
|
|
262
|
+
while (index < items.length) {
|
|
263
|
+
const currentIndex = index;
|
|
264
|
+
index += 1;
|
|
265
|
+
results[currentIndex] = await worker(items[currentIndex]);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
await Promise.all(Array.from({ length: effectiveConcurrency }, () => runWorker()));
|
|
269
|
+
return results;
|
|
270
|
+
};
|
|
271
|
+
const estimateRemoteRowSqlLength = (keys, row) => {
|
|
272
|
+
const delimitersLength = keys.length > 0 ? (keys.length - 1) * 2 : 0;
|
|
273
|
+
const valuesLength = keys.reduce((total, key) => {
|
|
274
|
+
return total + toSqlLiteral(row[key]).length;
|
|
275
|
+
}, 0);
|
|
276
|
+
return valuesLength + delimitersLength + 2;
|
|
277
|
+
};
|
|
278
|
+
const createInsertStatements = (targetType, settings, tableName, data) => {
|
|
279
|
+
if (data.length === 0) {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
const keys = Object.keys(data[0]);
|
|
283
|
+
const columnList = keys.map((key) => `\`${key}\``).join(', ');
|
|
284
|
+
const rowPlaceholder = `(${keys.map(() => '?').join(', ')})`;
|
|
285
|
+
const prefix = `INSERT INTO \`${tableName}\` (${columnList}) VALUES `;
|
|
286
|
+
const rowLimit = settings.rowsPerStatement;
|
|
287
|
+
const maxSqlLength = settings.maxStatementSqlLength;
|
|
288
|
+
const statements = [];
|
|
289
|
+
let batchRows = [];
|
|
290
|
+
let batchParameters = [];
|
|
291
|
+
let batchSqlLength = prefix.length;
|
|
292
|
+
const flushBatch = () => {
|
|
293
|
+
if (batchRows.length === 0) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
statements.push({
|
|
297
|
+
sql: `${prefix}${batchRows.map(() => rowPlaceholder).join(', ')}`,
|
|
298
|
+
parameters: batchParameters,
|
|
299
|
+
rowCount: batchRows.length,
|
|
300
|
+
});
|
|
301
|
+
batchRows = [];
|
|
302
|
+
batchParameters = [];
|
|
303
|
+
batchSqlLength = prefix.length;
|
|
304
|
+
};
|
|
305
|
+
for (const row of data) {
|
|
306
|
+
const rowParameters = keys.map((key) => row[key]);
|
|
307
|
+
const rowSqlLength = targetType === 'd1-remote' ? estimateRemoteRowSqlLength(keys, row) : rowPlaceholder.length;
|
|
308
|
+
const separatorLength = batchRows.length > 0 ? 2 : 0;
|
|
309
|
+
const nextSqlLength = batchSqlLength + separatorLength + rowSqlLength;
|
|
310
|
+
if (batchRows.length > 0 && (batchRows.length >= rowLimit || nextSqlLength > maxSqlLength)) {
|
|
311
|
+
flushBatch();
|
|
312
|
+
}
|
|
313
|
+
batchRows.push(row);
|
|
314
|
+
batchParameters.push(...rowParameters);
|
|
315
|
+
batchSqlLength += (batchRows.length > 1 ? 2 : 0) + rowSqlLength;
|
|
316
|
+
}
|
|
317
|
+
flushBatch();
|
|
318
|
+
return statements;
|
|
319
|
+
};
|
|
320
|
+
const createRemoteExecutionBatchesWithLimit = (statements, maxExecutionSqlLength) => {
|
|
321
|
+
if (statements.length <= 1) {
|
|
322
|
+
return statements;
|
|
323
|
+
}
|
|
324
|
+
const batches = [];
|
|
325
|
+
let sqlParts = [];
|
|
326
|
+
let parameters = [];
|
|
327
|
+
let rowCount = 0;
|
|
328
|
+
let currentLength = 0;
|
|
329
|
+
const flushBatch = () => {
|
|
330
|
+
if (sqlParts.length === 0) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
batches.push({
|
|
334
|
+
sql: sqlParts.join(';\n'),
|
|
335
|
+
parameters,
|
|
336
|
+
rowCount,
|
|
337
|
+
});
|
|
338
|
+
sqlParts = [];
|
|
339
|
+
parameters = [];
|
|
340
|
+
rowCount = 0;
|
|
341
|
+
currentLength = 0;
|
|
342
|
+
};
|
|
343
|
+
for (const statement of statements) {
|
|
344
|
+
const separatorLength = sqlParts.length > 0 ? 2 : 0;
|
|
345
|
+
const nextLength = currentLength + separatorLength + statement.sql.length;
|
|
346
|
+
if (sqlParts.length > 0 && nextLength > maxExecutionSqlLength) {
|
|
347
|
+
flushBatch();
|
|
348
|
+
}
|
|
349
|
+
sqlParts.push(statement.sql);
|
|
350
|
+
parameters.push(...statement.parameters);
|
|
351
|
+
rowCount += statement.rowCount;
|
|
352
|
+
currentLength += (sqlParts.length > 1 ? 2 : 0) + statement.sql.length;
|
|
353
|
+
}
|
|
354
|
+
flushBatch();
|
|
355
|
+
return batches;
|
|
356
|
+
};
|
|
97
357
|
const createRemoteD1Adapter = (database) => {
|
|
98
358
|
return {
|
|
99
359
|
async connect() {
|
|
@@ -107,38 +367,22 @@ const createRemoteD1Adapter = (database) => {
|
|
|
107
367
|
const output = WranglerD1.executeSql({ dbName: database, isLocal: false, sql: renderedSql });
|
|
108
368
|
const payload = extractWranglerJson(output);
|
|
109
369
|
if (payload === null || payload.length === 0) {
|
|
110
|
-
const rows =
|
|
370
|
+
const rows = parseWranglerTableRows(output);
|
|
111
371
|
return { rows, rowCount: rows.length };
|
|
112
372
|
}
|
|
113
|
-
const last = payload
|
|
373
|
+
const last = payload.at(-1);
|
|
374
|
+
if (last === undefined) {
|
|
375
|
+
return { rows: [], rowCount: 0 };
|
|
376
|
+
}
|
|
114
377
|
const rows = Array.isArray(last.results) ? last.results : [];
|
|
115
|
-
const
|
|
116
|
-
|
|
378
|
+
const totalChanges = payload.reduce((count, statement) => {
|
|
379
|
+
return count + (typeof statement.meta?.changes === 'number' ? statement.meta.changes : 0);
|
|
380
|
+
}, 0);
|
|
381
|
+
const rowCount = totalChanges > 0 ? totalChanges : rows.length;
|
|
117
382
|
return { rows, rowCount };
|
|
118
383
|
},
|
|
119
384
|
};
|
|
120
385
|
};
|
|
121
|
-
const redactConnectionString = (connectionString) => {
|
|
122
|
-
try {
|
|
123
|
-
const parsed = new URL(connectionString);
|
|
124
|
-
if (parsed.password.trim() !== '') {
|
|
125
|
-
parsed.password = '***';
|
|
126
|
-
}
|
|
127
|
-
return parsed.toString();
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return connectionString;
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
const getUrlPasswordForm = (connectionString) => {
|
|
134
|
-
try {
|
|
135
|
-
const parsed = new URL(connectionString);
|
|
136
|
-
return parsed.password || '';
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
386
|
const getErrorCause = (error) => {
|
|
143
387
|
if (error === null || typeof error !== 'object') {
|
|
144
388
|
return undefined;
|
|
@@ -190,11 +434,6 @@ const logDetailedError = (label, error) => {
|
|
|
190
434
|
logDetailedError(`${label} cause`, cause);
|
|
191
435
|
}
|
|
192
436
|
};
|
|
193
|
-
const describePasswordForLog = (password) => {
|
|
194
|
-
const hasSpecialCharacters = /[^a-zA-Z0-9]/.test(password);
|
|
195
|
-
const containsBang = password.includes('!');
|
|
196
|
-
return `len=${password.length}, special_chars=${hasSpecialCharacters}, contains_bang=${containsBang}`;
|
|
197
|
-
};
|
|
198
437
|
const normalizeNullLikeValue = (value) => {
|
|
199
438
|
if (typeof value !== 'string')
|
|
200
439
|
return value;
|
|
@@ -240,11 +479,6 @@ const createSourceAdapter = (config) => {
|
|
|
240
479
|
switch (config.sourceDriver) {
|
|
241
480
|
case 'mysql': {
|
|
242
481
|
const connectionDetails = parseConnectionDetails(config.sourceConnection, 3306, 'mysql', 'root');
|
|
243
|
-
const urlPasswordForm = getUrlPasswordForm(config.sourceConnection);
|
|
244
|
-
Logger.info(`[DataMigrator] Source password diagnostics: ${describePasswordForLog(connectionDetails.password)}`);
|
|
245
|
-
if (urlPasswordForm !== undefined) {
|
|
246
|
-
Logger.info(`[DataMigrator] MySQL password handoff: url_form(${describePasswordForLog(urlPasswordForm)}), final_auth(${describePasswordForLog(connectionDetails.password)}), matches=${urlPasswordForm === connectionDetails.password}`);
|
|
247
|
-
}
|
|
248
482
|
return MySQLAdapter.create({
|
|
249
483
|
driver: 'mysql',
|
|
250
484
|
host: connectionDetails.host,
|
|
@@ -335,14 +569,20 @@ export const DataMigrator = Object.freeze({
|
|
|
335
569
|
if (targetConnection.adapter) {
|
|
336
570
|
await DataMigrator.prepareTargetSchema(sourceConnection, targetConnection, config);
|
|
337
571
|
}
|
|
338
|
-
// Migrate each table sequentially for reliable D1/SQLite writes
|
|
339
572
|
Logger.info('Starting table migration...');
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
573
|
+
const tableLevels = buildTableMigrationLevels(schema.tables);
|
|
574
|
+
const tableParallelism = getTableParallelism(config, targetConnection);
|
|
575
|
+
for (const [levelIndex, tables] of tableLevels.entries()) {
|
|
576
|
+
Logger.info(`[DataMigrator] Starting table level ${levelIndex + 1}/${tableLevels.length}: ${tables.map((table) => table.name).join(', ')}`);
|
|
577
|
+
const levelResults = await executeWithConcurrency(tables, tableParallelism, async (table) => {
|
|
578
|
+
return DataMigrator.migrateTable(table, sourceConnection, targetConnection, config);
|
|
579
|
+
});
|
|
580
|
+
for (const [resultIndex, result] of levelResults.entries()) {
|
|
581
|
+
const table = tables[resultIndex];
|
|
582
|
+
progress.processedRows += result.rowsMigrated;
|
|
583
|
+
if (result.errors.length > 0 && table !== undefined) {
|
|
584
|
+
progress.errors[table.name] = result.errors.join('; ');
|
|
585
|
+
}
|
|
346
586
|
}
|
|
347
587
|
}
|
|
348
588
|
progress.totalRows = Math.max(progress.totalRows, progress.processedRows);
|
|
@@ -372,7 +612,6 @@ export const DataMigrator = Object.freeze({
|
|
|
372
612
|
*/
|
|
373
613
|
async connectToSource(config) {
|
|
374
614
|
Logger.info(`Connecting to ${config.sourceDriver} database...`);
|
|
375
|
-
Logger.info(`[DataMigrator] Source connection (redacted): ${redactConnectionString(config.sourceConnection)}`);
|
|
376
615
|
const adapter = createSourceAdapter(config);
|
|
377
616
|
try {
|
|
378
617
|
await adapter.connect();
|
|
@@ -473,6 +712,7 @@ export const DataMigrator = Object.freeze({
|
|
|
473
712
|
const tables = sourceSchema.tables.map((table) => ({
|
|
474
713
|
name: table.name,
|
|
475
714
|
rowCount: table.rowCount || 0,
|
|
715
|
+
dependsOn: table.foreignKeys.map((foreignKey) => foreignKey.referencedTable),
|
|
476
716
|
}));
|
|
477
717
|
Logger.info(`Found ${tables.length} tables`);
|
|
478
718
|
return { tables };
|
|
@@ -500,10 +740,10 @@ export const DataMigrator = Object.freeze({
|
|
|
500
740
|
Logger.info(`Migrating table: ${table.name}`);
|
|
501
741
|
const errors = [];
|
|
502
742
|
let rowsMigrated = 0;
|
|
743
|
+
const tableStartTime = Date.now();
|
|
503
744
|
try {
|
|
504
745
|
const totalRows = table.rowCount || 0;
|
|
505
746
|
const batchSize = config.batchSize || 1000;
|
|
506
|
-
// Check if table is already synced for resumability
|
|
507
747
|
const targetRowCount = await DataMigrator.getTargetRowCount(targetConnection, table.name);
|
|
508
748
|
if (targetRowCount >= totalRows) {
|
|
509
749
|
Logger.info(`Table ${table.name} already synced: ${targetRowCount}/${totalRows} rows, skipping`);
|
|
@@ -515,52 +755,54 @@ export const DataMigrator = Object.freeze({
|
|
|
515
755
|
else {
|
|
516
756
|
Logger.info(`Processing ${totalRows} rows in batches of ${batchSize}`);
|
|
517
757
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
try {
|
|
523
|
-
const chunk = await DataMigrator.readDataChunk(sourceConnection, table.name, offset, batchSize);
|
|
524
|
-
if (chunk.length === 0)
|
|
525
|
-
break;
|
|
526
|
-
// Transform data for D1 compatibility
|
|
527
|
-
const transformedChunk = await DataMigrator.transformData(chunk, table.name);
|
|
528
|
-
// Insert data into target
|
|
529
|
-
const insertedRows = await DataMigrator.insertData(targetConnection, table.name, transformedChunk);
|
|
530
|
-
if (insertedRows !== chunk.length) {
|
|
531
|
-
const verificationError = DataMigrator.createChunkVerificationError(table.name, offset, chunk.length, insertedRows);
|
|
532
|
-
throw ErrorFactory.createValidationError(`Chunk insert mismatch on ${table.name}`, verificationError);
|
|
533
|
-
}
|
|
534
|
-
rowsMigrated += insertedRows;
|
|
535
|
-
// Log progress for large tables
|
|
536
|
-
if (totalRows > 10000 && rowsMigrated % (batchSize * 10) === 0) {
|
|
537
|
-
const normalizedTotalRows = Math.max(totalRows, rowsMigrated);
|
|
538
|
-
const percentage = Math.round((rowsMigrated / normalizedTotalRows) * 100);
|
|
539
|
-
Logger.info(`Table ${table.name}: ${rowsMigrated}/${normalizedTotalRows} (${percentage}%)`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
catch (error) {
|
|
543
|
-
const errorMsg = `Chunk processing failed at offset ${offset}: ${error}`;
|
|
544
|
-
Logger.error(errorMsg);
|
|
545
|
-
errors.push(errorMsg);
|
|
546
|
-
// Continue with next chunk instead of failing completely
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
Logger.info(`Table ${table.name} completed: ${rowsMigrated} rows migrated`);
|
|
758
|
+
rowsMigrated = await DataMigrator.processTableChunks(table, sourceConnection, targetConnection, totalRows, batchSize, targetRowCount, errors);
|
|
759
|
+
const tableDurationMs = Date.now() - tableStartTime;
|
|
760
|
+
Logger.info(`[DataMigrator] Table ${table.name} completed rows=${rowsMigrated}/${totalRows} duration=${formatDuration(tableDurationMs)} rate=${formatRowsPerSecond(rowsMigrated, tableDurationMs)}`);
|
|
761
|
+
return { rowsMigrated, errors };
|
|
551
762
|
}
|
|
552
763
|
catch (error) {
|
|
553
|
-
const errorMsg = `
|
|
764
|
+
const errorMsg = `Table migration failed for ${table.name}: ${error}`;
|
|
554
765
|
Logger.error(errorMsg);
|
|
555
766
|
errors.push(errorMsg);
|
|
767
|
+
return { rowsMigrated, errors };
|
|
768
|
+
}
|
|
769
|
+
},
|
|
770
|
+
async processTableChunks(table, sourceConnection, targetConnection, totalRows, batchSize, startOffset, errors) {
|
|
771
|
+
let rowsMigrated = 0;
|
|
772
|
+
for (let offset = startOffset; offset < totalRows; offset += batchSize) {
|
|
773
|
+
try {
|
|
774
|
+
const chunkStartTime = Date.now();
|
|
775
|
+
const chunk = await DataMigrator.readDataChunk(sourceConnection, table.name, offset, batchSize);
|
|
776
|
+
if (chunk.length === 0) {
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
const transformedChunk = await DataMigrator.transformData(chunk, table.name);
|
|
780
|
+
const insertedRows = await DataMigrator.insertData(targetConnection, table.name, transformedChunk);
|
|
781
|
+
if (insertedRows !== chunk.length) {
|
|
782
|
+
const verificationError = DataMigrator.createChunkVerificationError(table.name, offset, chunk.length, insertedRows);
|
|
783
|
+
throw ErrorFactory.createValidationError(`Chunk insert mismatch on ${table.name}`, verificationError);
|
|
784
|
+
}
|
|
785
|
+
rowsMigrated += insertedRows;
|
|
786
|
+
const chunkDurationMs = Date.now() - chunkStartTime;
|
|
787
|
+
Logger.info(`[DataMigrator] Chunk ${table.name} offset=${offset} rows=${insertedRows} duration=${formatDuration(chunkDurationMs)} rate=${formatRowsPerSecond(insertedRows, chunkDurationMs)}`);
|
|
788
|
+
if (totalRows > 10000 && rowsMigrated % (batchSize * 10) === 0) {
|
|
789
|
+
const normalizedTotalRows = Math.max(totalRows, rowsMigrated);
|
|
790
|
+
const percentage = Math.round((rowsMigrated / normalizedTotalRows) * 100);
|
|
791
|
+
Logger.info(`Table ${table.name}: ${rowsMigrated}/${normalizedTotalRows} (${percentage}%)`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
const errorMsg = `Chunk processing failed at offset ${offset}: ${error}`;
|
|
796
|
+
Logger.error(errorMsg);
|
|
797
|
+
errors.push(errorMsg);
|
|
798
|
+
}
|
|
556
799
|
}
|
|
557
|
-
return
|
|
800
|
+
return rowsMigrated;
|
|
558
801
|
},
|
|
559
802
|
/**
|
|
560
803
|
* Read data chunk from source database
|
|
561
804
|
*/
|
|
562
805
|
async readDataChunk(sourceConnection, tableName, offset, batchSize) {
|
|
563
|
-
Logger.debug(`Reading chunk from ${tableName}: offset ${offset}, size ${batchSize}`);
|
|
564
806
|
if (!sourceConnection.adapter)
|
|
565
807
|
return [];
|
|
566
808
|
try {
|
|
@@ -576,8 +818,7 @@ export const DataMigrator = Object.freeze({
|
|
|
576
818
|
/**
|
|
577
819
|
* Transform data for D1 compatibility
|
|
578
820
|
*/
|
|
579
|
-
async transformData(chunk,
|
|
580
|
-
Logger.debug(`Transforming ${chunk.length} rows for table ${tableName}`);
|
|
821
|
+
async transformData(chunk, _tableName) {
|
|
581
822
|
return chunk.map((row) => {
|
|
582
823
|
const transformed = {};
|
|
583
824
|
for (const [key, rawValue] of Object.entries(row)) {
|
|
@@ -612,27 +853,30 @@ export const DataMigrator = Object.freeze({
|
|
|
612
853
|
* Insert data into target database
|
|
613
854
|
*/
|
|
614
855
|
async insertData(targetConnection, tableName, data) {
|
|
615
|
-
Logger.debug(`Inserting ${data.length} rows into ${tableName}`);
|
|
616
856
|
if (data.length === 0)
|
|
617
857
|
return 0;
|
|
618
858
|
if (!targetConnection.adapter) {
|
|
619
859
|
throw ErrorFactory.createValidationError(`No target adapter configured for ${targetConnection.database}`);
|
|
620
860
|
}
|
|
621
|
-
const
|
|
622
|
-
const
|
|
623
|
-
const
|
|
624
|
-
|
|
861
|
+
const batchSettings = getInsertBatchSettings(targetConnection);
|
|
862
|
+
const statements = createInsertStatements(targetConnection.type, batchSettings, tableName, data);
|
|
863
|
+
const executableStatements = targetConnection.type === 'd1-remote'
|
|
864
|
+
? createRemoteExecutionBatchesWithLimit(statements, batchSettings.maxExecutionSqlLength)
|
|
865
|
+
: statements;
|
|
625
866
|
let insertedRows = 0;
|
|
626
|
-
for (const
|
|
627
|
-
const values = keys.map((key) => row[key]);
|
|
867
|
+
for (const statement of executableStatements) {
|
|
628
868
|
try {
|
|
629
|
-
|
|
630
|
-
|
|
869
|
+
const executionStartTime = Date.now();
|
|
870
|
+
const result = await targetConnection.adapter.query(statement.sql, statement.parameters);
|
|
871
|
+
const executionDurationMs = Date.now() - executionStartTime;
|
|
872
|
+
const affectedRows = typeof result.rowCount === 'number' ? result.rowCount : statement.rowCount;
|
|
873
|
+
insertedRows += affectedRows;
|
|
874
|
+
adjustRemoteBatchTuning(targetConnection, affectedRows, executionDurationMs, statement.sql.length);
|
|
631
875
|
}
|
|
632
876
|
catch (error) {
|
|
633
877
|
throw ErrorFactory.createValidationError(`Insert failed for table ${tableName}`, {
|
|
634
|
-
sql,
|
|
635
|
-
|
|
878
|
+
sql: statement.sql,
|
|
879
|
+
rowCount: statement.rowCount,
|
|
636
880
|
cause: error,
|
|
637
881
|
});
|
|
638
882
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MigrateToD1Command.d.ts","sourceRoot":"","sources":["../../src/cli/MigrateToD1Command.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAe,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,UAAU,CAAC;AAOxE,KAAK,iBAAiB,GAAG;IACvB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,IAAI,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B,CAAC;
|
|
1
|
+
{"version":3,"file":"MigrateToD1Command.d.ts","sourceRoot":"","sources":["../../src/cli/MigrateToD1Command.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAe,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,UAAU,CAAC;AAOxE,KAAK,iBAAiB,GAAG;IACvB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,IAAI,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B,CAAC;AAouBF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,iBA0G/B,CAAC;AAEH;;GAEG;AACH,iBAAe,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAetE;AAED;;GAEG;AACH,iBAAe,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAmGxE;AAED;;GAEG;AACH,iBAAe,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAkGtE;AAED;;GAEG;AACH,iBAAS,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA2BrF;AAGD,eAAO,MAAM,iBAAiB;;;;;EAK5B,CAAC"}
|
|
@@ -229,35 +229,6 @@ const normalizeSourceConnectionString = (sourceConnection, sourceDriver, origin)
|
|
|
229
229
|
}
|
|
230
230
|
return buildNetworkConnectionString(details);
|
|
231
231
|
};
|
|
232
|
-
const redactConnectionString = (sourceConnection) => {
|
|
233
|
-
try {
|
|
234
|
-
const parsed = new URL(sourceConnection);
|
|
235
|
-
if (parsed.password.trim() !== '') {
|
|
236
|
-
parsed.password = '***';
|
|
237
|
-
}
|
|
238
|
-
return parsed.toString();
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
return sourceConnection;
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
const getPasswordForDiagnostics = (sourceConnection, sourceDriver, origin) => {
|
|
245
|
-
if (origin === 'db-env') {
|
|
246
|
-
return parseNetworkConnectionDetails(sourceConnection, sourceDriver)?.password;
|
|
247
|
-
}
|
|
248
|
-
try {
|
|
249
|
-
const parsed = new URL(sourceConnection);
|
|
250
|
-
return parsed.password;
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
return undefined;
|
|
254
|
-
}
|
|
255
|
-
};
|
|
256
|
-
const describePasswordForLog = (password) => {
|
|
257
|
-
const hasSpecialCharacters = /[^a-zA-Z0-9]/.test(password);
|
|
258
|
-
const containsBang = password.includes('!');
|
|
259
|
-
return `len=${password.length}, special_chars=${hasSpecialCharacters}, contains_bang=${containsBang}`;
|
|
260
|
-
};
|
|
261
232
|
const getErrorCause = (error) => {
|
|
262
233
|
if (error === null || typeof error !== 'object') {
|
|
263
234
|
return undefined;
|
|
@@ -309,18 +280,6 @@ const logDetailedError = (label, error) => {
|
|
|
309
280
|
logDetailedError(`${label} cause`, cause);
|
|
310
281
|
}
|
|
311
282
|
};
|
|
312
|
-
const logSourceConnectionDiagnostics = (sourceDriver, sourceConnection, origin, originalValue) => {
|
|
313
|
-
Logger.info(`[d1-migrator] Source connection origin: ${origin}`);
|
|
314
|
-
Logger.info(`[d1-migrator] Source connection driver: ${sourceDriver}`);
|
|
315
|
-
Logger.info(`[d1-migrator] Source connection (redacted): ${redactConnectionString(sourceConnection)}`);
|
|
316
|
-
const originalPassword = getPasswordForDiagnostics(originalValue, sourceDriver, origin);
|
|
317
|
-
const finalPassword = getPasswordForDiagnostics(sourceConnection, sourceDriver, origin);
|
|
318
|
-
if (originalPassword === undefined || finalPassword === undefined) {
|
|
319
|
-
Logger.info('[d1-migrator] Source connection diagnostics: non-network source or unable to parse URL');
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
Logger.info(`[d1-migrator] Source password diagnostics: provided(${describePasswordForLog(originalPassword)}), final(${describePasswordForLog(finalPassword)}), matches=${originalPassword === finalPassword}`);
|
|
323
|
-
};
|
|
324
283
|
const normalizeSourceDriver = (value) => {
|
|
325
284
|
if (value === undefined) {
|
|
326
285
|
return undefined;
|
|
@@ -578,8 +537,7 @@ export const MigrateToD1Command = BaseCommand.create({
|
|
|
578
537
|
execute: async (options) => {
|
|
579
538
|
try {
|
|
580
539
|
Logger.info('Starting D1 migration process...');
|
|
581
|
-
const { config, schemaOnly
|
|
582
|
-
logSourceConnectionDiagnostics(config.sourceDriver, config.sourceConnection, sourceConnectionOrigin, originalSourceConnection);
|
|
540
|
+
const { config, schemaOnly } = resolveMigrationConfig(options);
|
|
583
541
|
const configValidation = validateConfig(config);
|
|
584
542
|
if (!configValidation.valid) {
|
|
585
543
|
throw ErrorFactory.createValidationError(`Invalid migration configuration: ${configValidation.errors.join(', ')}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/d1-migrator",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.3",
|
|
4
4
|
"description": "Resumable database migration toolkit for moving data to Cloudflare D1 with ZinTrust.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@zintrust/db-d1": "^1.8.0",
|
|
44
|
-
"@zintrust/db-mysql": "^1.
|
|
44
|
+
"@zintrust/db-mysql": "^1.9.0",
|
|
45
45
|
"@zintrust/db-postgres": "^1.8.0",
|
|
46
46
|
"@zintrust/db-sqlite": "^1.8.0",
|
|
47
47
|
"@zintrust/db-sqlserver": "^1.8.0"
|