@zintrust/d1-migrator 2.0.0 → 2.0.2

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.
@@ -1 +1 @@
1
- {"version":3,"file":"DataMigrator.d.ts","sourceRoot":"","sources":["../../src/cli/DataMigrator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"DataMigrator.d.ts","sourceRoot":"","sources":["../../src/cli/DataMigrator.ts"],"names":[],"mappings":"AACA;;;GAGG;AAaH,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;IAC1B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC;AAED,KAAK,uBAAuB,GAAG;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,gBAAgB,EAAE,uBAAuB,EAAE,CAAC;CAC7C,CAAC;AAEF,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;IAC1B,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;CACvC;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;CACtB;AASD,KAAK,iBAAiB,GAAG;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,qBAAqB,EAAE,MAAM,CAAC;CAC/B,CAAC;AAQF,KAAK,kBAAkB,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACxE,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAyrCF;;;GAGG;AACH,eAAO,MAAM,YAAY;IACvB;;OAEG;wBACuB,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAmGtE;;OAEG;4BAC2B,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAiCzE;;OAEG;4BAC2B,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA6CzE;;OAEG;0CAEiB,gBAAgB,oBAChB,gBAAgB,UAC1B,eAAe,GACtB,OAAO,CAAC,IAAI,CAAC;IA+EhB;;OAEG;+BAC8B,gBAAgB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,SAAS,EAAE,CAAA;KAAE,CAAC;IAoBpF;;OAEG;wCACuC,gBAAgB,aAAa,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgB/F;;OAEG;wBAEM,SAAS,oBACE,gBAAgB,oBAChB,gBAAgB,UAC1B,eAAe,GACtB,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;8BA0E7C,SAAS,oBACE,gBAAgB,oBAChB,gBAAgB,aACvB,MAAM,aACN,MAAM,eACJ,MAAM,UACX,MAAM,EAAE,GACf,OAAO,CAAC,MAAM,CAAC;IA8BlB;;OAEG;oCAEiB,gBAAgB,aACvB,MAAM,UACT,MAAM,aACH,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IA8BrC;;OAEG;yBAEM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cACpB,MAAM,GACjB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IA0CrC;;OAEG;iCAEiB,gBAAgB,aACvB,MAAM,QACX,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAC9B,OAAO,CAAC,MAAM,CAAC;IAyClB;;OAEG;gCACyB,eAAe,CAAC,cAAc,CAAC,aAAa,MAAM,GAAG,MAAM;IAavF;;OAEG;wCAEM,MAAM,UACL,MAAM,gBACA,MAAM,gBACN,MAAM,GACnB,0BAA0B;IAS7B;;OAEG;gCACyB,MAAM,GAAG,iBAAiB;IAetD;;OAEG;6BAES,iBAAiB,WAClB,OAAO,CAAC,iBAAiB,CAAC,GAClC,iBAAiB;EAGpB,CAAC"}
@@ -4,6 +4,8 @@
4
4
  * Handles the actual data migration between databases
5
5
  */
6
6
  import { ErrorFactory, LocalD1Resolver, Logger, WranglerD1 } from '@zintrust/core';
7
+ import { appendFileSync, mkdirSync } from 'node:fs';
8
+ import * as path from 'node:path';
7
9
  import { MySQLAdapter } from '@zintrust/db-mysql';
8
10
  import { PostgreSQLAdapter } from '@zintrust/db-postgres';
9
11
  import { SQLiteAdapter } from '@zintrust/db-sqlite';
@@ -134,17 +136,17 @@ const bindSqlParameters = (sql, parameters) => {
134
136
  };
135
137
  const REMOTE_INSERT_ROWS_PER_STATEMENT = 1000;
136
138
  const LOCAL_INSERT_ROWS_PER_STATEMENT = 500;
137
- const MAX_REMOTE_INSERT_SQL_LENGTH = 300000;
138
- const MAX_REMOTE_EXECUTION_SQL_LENGTH = 800000;
139
- const MIN_REMOTE_INSERT_ROWS_PER_STATEMENT = 100;
140
- const MAX_REMOTE_INSERT_ROWS_PER_STATEMENT = 3000;
141
- const DEFAULT_REMOTE_TABLE_PARALLELISM = 8;
142
- const DEFAULT_SOURCE_READ_BATCH_SIZE = 250;
143
- const MIN_SOURCE_READ_BATCH_SIZE = 50;
144
- const MAX_SOURCE_READ_BATCH_SIZE = 1500;
145
- const SOURCE_READ_TARGET_TIME_MS = 5000;
139
+ const MAX_REMOTE_INSERT_ROWS_PER_STATEMENT = 2000;
140
+ const MAX_REMOTE_INSERT_SQL_LENGTH = 95 * 1024;
141
+ const MAX_REMOTE_EXECUTION_SQL_LENGTH = MAX_REMOTE_INSERT_SQL_LENGTH;
142
+ const MIN_REMOTE_INSERT_ROWS_PER_STATEMENT = 1;
143
+ const DEFAULT_REMOTE_TABLE_PARALLELISM = 1;
144
+ const DEFAULT_SOURCE_READ_BATCH_SIZE = 1000;
145
+ const MIN_SOURCE_READ_BATCH_SIZE = 1;
146
146
  const SOURCE_CONNECT_RETRY_ATTEMPTS = 3;
147
147
  const SOURCE_CONNECT_RETRY_BASE_DELAY_MS = 500;
148
+ const FAILED_TABLE_REPORT_PATH = path.join('logs', 'd1-migration-failed-report.log');
149
+ const REMOTE_SQL_GROW_THRESHOLD_RATIO = 0.6;
148
150
  const formatDuration = (durationMs) => {
149
151
  if (durationMs < 1000) {
150
152
  return `${durationMs}ms`;
@@ -158,6 +160,9 @@ const formatRowsPerSecond = (rows, durationMs) => {
158
160
  const rate = rows / (durationMs / 1000);
159
161
  return `${rate >= 100 ? rate.toFixed(0) : rate.toFixed(2)} rows/s`;
160
162
  };
163
+ const getSqlByteLength = (value) => {
164
+ return Buffer.byteLength(value, 'utf8');
165
+ };
161
166
  const waitMs = async (durationMs) => {
162
167
  await new Promise((resolve) => {
163
168
  const start = Date.now();
@@ -171,24 +176,55 @@ const waitMs = async (durationMs) => {
171
176
  poll();
172
177
  });
173
178
  };
174
- const calculateOptimalBatchSize = (measuredTimePerRowMs, targetTimePerBatchMs = 5000, minBatchSize = 100, maxBatchSize = 3000) => {
175
- if (measuredTimePerRowMs <= 0) {
176
- return minBatchSize;
177
- }
178
- const calculatedSize = Math.floor(targetTimePerBatchMs / measuredTimePerRowMs);
179
- return Math.max(minBatchSize, Math.min(maxBatchSize, calculatedSize));
179
+ const normalizeSourceBatchSize = (requestedBatchSize) => {
180
+ return Math.max(MIN_SOURCE_READ_BATCH_SIZE, requestedBatchSize);
181
+ };
182
+ const createSourceBatchTuning = (initialBatchSize) => {
183
+ return {
184
+ readBatchSize: normalizeSourceBatchSize(initialBatchSize),
185
+ readBatchHistory: [],
186
+ };
187
+ };
188
+ const createRemoteBatchTuning = () => {
189
+ return {
190
+ rowsPerStatement: REMOTE_INSERT_ROWS_PER_STATEMENT,
191
+ maxStatementSqlLength: MAX_REMOTE_INSERT_SQL_LENGTH,
192
+ maxExecutionSqlLength: MAX_REMOTE_EXECUTION_SQL_LENGTH,
193
+ };
194
+ };
195
+ const sanitizeFailureReason = (reason) => {
196
+ return reason.replaceAll(/\s+/g, ' ').trim();
197
+ };
198
+ const appendFailedTableReport = (params) => {
199
+ const reportPath = path.join(process.cwd(), FAILED_TABLE_REPORT_PATH);
200
+ const reportDir = path.dirname(reportPath);
201
+ const reason = sanitizeFailureReason(params.errors.at(-1) ?? 'Unknown migration failure');
202
+ const entry = JSON.stringify({
203
+ timestamp: new Date().toISOString(),
204
+ migrationId: params.migrationId,
205
+ targetDatabase: params.targetDatabase,
206
+ tableName: params.tableName,
207
+ rowsMigrated: params.rowsMigrated,
208
+ totalRows: params.totalRows,
209
+ reason,
210
+ });
211
+ mkdirSync(reportDir, { recursive: true });
212
+ appendFileSync(reportPath, `${entry}\n`, 'utf8');
213
+ Logger.warn(`[DataMigrator] Skipped remaining rows for ${params.tableName}. Failure recorded in ${FAILED_TABLE_REPORT_PATH}`);
180
214
  };
181
215
  const getSourceBatchTuning = (connection, initialBatchSize) => {
182
216
  if (connection.sourceBatchTuning !== undefined) {
183
217
  return connection.sourceBatchTuning;
184
218
  }
185
- const startingBatchSize = Math.max(MIN_SOURCE_READ_BATCH_SIZE, Math.min(DEFAULT_SOURCE_READ_BATCH_SIZE, initialBatchSize));
186
- connection.sourceBatchTuning = {
187
- readBatchSize: startingBatchSize,
188
- readBatchHistory: [],
189
- };
219
+ connection.sourceBatchTuning = createSourceBatchTuning(initialBatchSize);
190
220
  return connection.sourceBatchTuning;
191
221
  };
222
+ const resetSourceBatchTuning = (connection, initialBatchSize) => {
223
+ if (connection.driver !== 'mysql') {
224
+ return;
225
+ }
226
+ connection.sourceBatchTuning = createSourceBatchTuning(initialBatchSize);
227
+ };
192
228
  const isRetryableConnectionError = (error) => {
193
229
  const message = getErrorMessage(error).toLowerCase();
194
230
  return (message.includes('etimedout') ||
@@ -229,35 +265,31 @@ const updateSourceBatchTuningAfterSuccess = (connection, rowsRead, durationMs, b
229
265
  if (tuning.readBatchHistory.length > 10) {
230
266
  tuning.readBatchHistory.shift();
231
267
  }
232
- const avgTimePerRow = tuning.readBatchHistory.reduce((sum, entry) => sum + entry.timePerRow, 0) /
233
- tuning.readBatchHistory.length;
234
- const targetTimePerBatchMs = Math.max(1000, Math.min(SOURCE_READ_TARGET_TIME_MS, Math.floor(batchSize * 5)));
235
- const optimalSize = calculateOptimalBatchSize(avgTimePerRow, targetTimePerBatchMs, MIN_SOURCE_READ_BATCH_SIZE, MAX_SOURCE_READ_BATCH_SIZE);
236
- if (Math.abs(optimalSize - tuning.readBatchSize) >= 50) {
237
- tuning.readBatchSize = optimalSize;
238
- Logger.info(`[DataMigrator] Adaptive source batching: adjusted to ${optimalSize} rows (avg ${avgTimePerRow.toFixed(2)}ms/row)`);
239
- }
240
268
  };
241
269
  const reduceSourceBatchSizeAfterTimeout = (connection, batchSize) => {
242
270
  const tuning = getSourceBatchTuning(connection, batchSize);
243
- const reducedSize = Math.max(MIN_SOURCE_READ_BATCH_SIZE, Math.floor(Math.min(tuning.readBatchSize, batchSize) / 2));
244
- if (reducedSize < tuning.readBatchSize) {
245
- tuning.readBatchSize = reducedSize;
246
- Logger.warn(`[DataMigrator] Adaptive source batching: reduced to ${reducedSize} rows after timeout-like failure`);
271
+ const currentSize = Math.min(tuning.readBatchSize, batchSize);
272
+ if (currentSize <= MIN_SOURCE_READ_BATCH_SIZE) {
273
+ return 0;
247
274
  }
275
+ const reducedSize = Math.max(MIN_SOURCE_READ_BATCH_SIZE, Math.floor(currentSize / 2));
276
+ tuning.readBatchSize = reducedSize;
277
+ Logger.warn(`[DataMigrator] Source batch retry: ${currentSize} -> ${reducedSize} rows after retryable read failure`);
248
278
  return reducedSize;
249
279
  };
250
280
  const getRemoteBatchTuning = (connection) => {
251
281
  if (connection.remoteBatchTuning !== undefined) {
252
282
  return connection.remoteBatchTuning;
253
283
  }
254
- connection.remoteBatchTuning = {
255
- rowsPerStatement: REMOTE_INSERT_ROWS_PER_STATEMENT,
256
- maxStatementSqlLength: MAX_REMOTE_INSERT_SQL_LENGTH,
257
- maxExecutionSqlLength: MAX_REMOTE_EXECUTION_SQL_LENGTH,
258
- };
284
+ connection.remoteBatchTuning = createRemoteBatchTuning();
259
285
  return connection.remoteBatchTuning;
260
286
  };
287
+ const resetRemoteBatchTuning = (connection) => {
288
+ if (connection.type !== 'd1-remote') {
289
+ return;
290
+ }
291
+ connection.remoteBatchTuning = createRemoteBatchTuning();
292
+ };
261
293
  const getInsertBatchSettings = (connection) => {
262
294
  if (connection.type === 'd1-remote') {
263
295
  const tuning = getRemoteBatchTuning(connection);
@@ -279,33 +311,32 @@ const adjustRemoteBatchTuning = (connection, executedRows, durationMs, sqlLength
279
311
  }
280
312
  const tuning = getRemoteBatchTuning(connection);
281
313
  const previousRowsPerStatement = tuning.rowsPerStatement;
282
- const nearSqlLimit = sqlLength >= Math.floor(tuning.maxExecutionSqlLength * 0.9);
283
- if (durationMs <= 2500 &&
284
- executedRows >= Math.floor(previousRowsPerStatement * 0.8) &&
285
- !nearSqlLimit) {
286
- tuning.rowsPerStatement = Math.min(MAX_REMOTE_INSERT_ROWS_PER_STATEMENT, Math.max(previousRowsPerStatement + 50, Math.floor(previousRowsPerStatement * 1.3)));
287
- }
288
- else if (durationMs >= 10000 || nearSqlLimit) {
289
- tuning.rowsPerStatement = Math.max(MIN_REMOTE_INSERT_ROWS_PER_STATEMENT, Math.min(previousRowsPerStatement - 50, Math.floor(previousRowsPerStatement * 0.8)));
314
+ const growthThreshold = Math.floor(tuning.maxStatementSqlLength * REMOTE_SQL_GROW_THRESHOLD_RATIO);
315
+ if (previousRowsPerStatement >= MAX_REMOTE_INSERT_ROWS_PER_STATEMENT ||
316
+ executedRows < previousRowsPerStatement ||
317
+ sqlLength > growthThreshold ||
318
+ durationMs > 10000) {
319
+ return;
290
320
  }
321
+ tuning.rowsPerStatement = Math.min(MAX_REMOTE_INSERT_ROWS_PER_STATEMENT, previousRowsPerStatement * 2);
291
322
  if (tuning.rowsPerStatement !== previousRowsPerStatement) {
292
- Logger.info(`[DataMigrator] Adaptive remote batching: rows_per_statement ${previousRowsPerStatement} -> ${tuning.rowsPerStatement} after ${executedRows} rows in ${formatDuration(durationMs)}`);
323
+ Logger.info(`[DataMigrator] Remote batch growth: rows_per_statement ${previousRowsPerStatement} -> ${tuning.rowsPerStatement} after ${executedRows} rows in ${formatDuration(durationMs)} with sql_size=${sqlLength}/${tuning.maxStatementSqlLength} bytes`);
293
324
  }
294
325
  };
295
- const reduceRemoteBatchTuningAfterFailure = (connection, sqlLength) => {
326
+ const reduceRemoteBatchTuningAfterFailure = (connection) => {
296
327
  if (connection.type !== 'd1-remote') {
297
- return;
328
+ return 0;
298
329
  }
299
330
  const tuning = getRemoteBatchTuning(connection);
300
331
  const previousRowsPerStatement = tuning.rowsPerStatement;
301
- const nearSqlLimit = sqlLength >= Math.floor(tuning.maxExecutionSqlLength * 0.8);
302
- if (!nearSqlLimit && previousRowsPerStatement <= MIN_REMOTE_INSERT_ROWS_PER_STATEMENT) {
303
- return;
332
+ if (previousRowsPerStatement <= MIN_REMOTE_INSERT_ROWS_PER_STATEMENT) {
333
+ return 0;
304
334
  }
305
- tuning.rowsPerStatement = Math.max(MIN_REMOTE_INSERT_ROWS_PER_STATEMENT, Math.min(previousRowsPerStatement - 50, Math.floor(previousRowsPerStatement * 0.5)));
335
+ tuning.rowsPerStatement = Math.max(MIN_REMOTE_INSERT_ROWS_PER_STATEMENT, Math.floor(previousRowsPerStatement / 2));
306
336
  if (tuning.rowsPerStatement !== previousRowsPerStatement) {
307
337
  Logger.warn(`[DataMigrator] Adaptive remote batching: rows_per_statement ${previousRowsPerStatement} -> ${tuning.rowsPerStatement} after failed remote insert`);
308
338
  }
339
+ return tuning.rowsPerStatement;
309
340
  };
310
341
  const logTableMigrationProgress = (table, rowsMigrated, totalRows, batchSize) => {
311
342
  if (totalRows <= 10000 || rowsMigrated % (batchSize * 10) !== 0) {
@@ -407,18 +438,20 @@ const buildChunkProcessingFailureResult = (sourceConnection, offset, batchSize,
407
438
  errors.push(errorMsg);
408
439
  if (sourceConnection.driver === 'mysql' && isRetryableConnectionError(error)) {
409
440
  const reducedBatchSize = reduceSourceBatchSizeAfterTimeout(sourceConnection, batchSize);
410
- return {
411
- rowsMigrated: 0,
412
- nextOffset: offset,
413
- nextBatchSize: reducedBatchSize,
414
- continueProcessing: reducedBatchSize >= batchSize,
415
- };
441
+ if (reducedBatchSize > 0) {
442
+ return {
443
+ rowsMigrated: 0,
444
+ nextOffset: offset,
445
+ nextBatchSize: reducedBatchSize,
446
+ continueProcessing: true,
447
+ };
448
+ }
416
449
  }
417
450
  return {
418
451
  rowsMigrated: 0,
419
- nextOffset: offset + batchSize,
452
+ nextOffset: offset,
420
453
  nextBatchSize: batchSize,
421
- continueProcessing: true,
454
+ continueProcessing: false,
422
455
  };
423
456
  };
424
457
  const processTableChunk = async (table, sourceConnection, targetConnection, offset, batchSize, errors) => {
@@ -503,7 +536,7 @@ const executeWithConcurrency = async (items, concurrency, worker) => {
503
536
  const estimateRemoteRowSqlLength = (keys, row) => {
504
537
  const delimitersLength = keys.length > 0 ? (keys.length - 1) * 2 : 0;
505
538
  const valuesLength = keys.reduce((total, key) => {
506
- return total + toSqlLiteral(row[key]).length;
539
+ return total + getSqlByteLength(toSqlLiteral(row[key]));
507
540
  }, 0);
508
541
  return valuesLength + delimitersLength + 2;
509
542
  };
@@ -520,7 +553,7 @@ const createInsertStatements = (targetType, settings, tableName, data) => {
520
553
  const statements = [];
521
554
  let batchRows = [];
522
555
  let batchParameters = [];
523
- let batchSqlLength = prefix.length;
556
+ let batchSqlLength = getSqlByteLength(prefix);
524
557
  const flushBatch = () => {
525
558
  if (batchRows.length === 0) {
526
559
  return;
@@ -529,14 +562,17 @@ const createInsertStatements = (targetType, settings, tableName, data) => {
529
562
  sql: `${prefix}${batchRows.map(() => rowPlaceholder).join(', ')}`,
530
563
  parameters: batchParameters,
531
564
  rowCount: batchRows.length,
565
+ rows: batchRows,
532
566
  });
533
567
  batchRows = [];
534
568
  batchParameters = [];
535
- batchSqlLength = prefix.length;
569
+ batchSqlLength = getSqlByteLength(prefix);
536
570
  };
537
571
  for (const row of data) {
538
572
  const rowParameters = keys.map((key) => row[key]);
539
- const rowSqlLength = targetType === 'd1-remote' ? estimateRemoteRowSqlLength(keys, row) : rowPlaceholder.length;
573
+ const rowSqlLength = targetType === 'd1-remote'
574
+ ? estimateRemoteRowSqlLength(keys, row)
575
+ : getSqlByteLength(rowPlaceholder);
540
576
  const separatorLength = batchRows.length > 0 ? 2 : 0;
541
577
  const nextSqlLength = batchSqlLength + separatorLength + rowSqlLength;
542
578
  if (batchRows.length > 0 && (batchRows.length >= rowLimit || nextSqlLength > maxSqlLength)) {
@@ -549,42 +585,59 @@ const createInsertStatements = (targetType, settings, tableName, data) => {
549
585
  flushBatch();
550
586
  return statements;
551
587
  };
552
- const createRemoteExecutionBatchesWithLimit = (statements, maxExecutionSqlLength) => {
553
- if (statements.length <= 1) {
554
- return statements;
555
- }
556
- const batches = [];
557
- let sqlParts = [];
558
- let parameters = [];
559
- let rowCount = 0;
560
- let currentLength = 0;
561
- const flushBatch = () => {
562
- if (sqlParts.length === 0) {
563
- return;
588
+ const executeInsertStatement = async (targetConnection, statement) => {
589
+ if (!targetConnection.adapter) {
590
+ throw ErrorFactory.createValidationError(`No target adapter configured for ${targetConnection.database}`);
591
+ }
592
+ const executionStartTime = Date.now();
593
+ const result = await targetConnection.adapter.query(statement.sql, statement.parameters);
594
+ const executionDurationMs = Date.now() - executionStartTime;
595
+ const affectedRows = typeof result.rowCount === 'number' ? result.rowCount : statement.rowCount;
596
+ return { affectedRows, executionDurationMs };
597
+ };
598
+ const createRemoteInsertExecutionQueue = (targetConnection, tableName, rows) => {
599
+ const batchSettings = getInsertBatchSettings(targetConnection);
600
+ return createInsertStatements(targetConnection.type, batchSettings, tableName, rows);
601
+ };
602
+ const insertRemoteRowsWithRetry = async (targetConnection, tableName, data) => {
603
+ const pendingRows = [data];
604
+ let insertedRows = 0;
605
+ while (pendingRows.length > 0) {
606
+ const currentRows = pendingRows.shift();
607
+ if (currentRows === undefined || currentRows.length === 0) {
608
+ continue;
564
609
  }
565
- batches.push({
566
- sql: sqlParts.join(';\n'),
567
- parameters,
568
- rowCount,
569
- });
570
- sqlParts = [];
571
- parameters = [];
572
- rowCount = 0;
573
- currentLength = 0;
574
- };
575
- for (const statement of statements) {
576
- const separatorLength = sqlParts.length > 0 ? 2 : 0;
577
- const nextLength = currentLength + separatorLength + statement.sql.length;
578
- if (sqlParts.length > 0 && nextLength > maxExecutionSqlLength) {
579
- flushBatch();
610
+ const statements = createRemoteInsertExecutionQueue(targetConnection, tableName, currentRows);
611
+ let shouldRetryCurrentRows = false;
612
+ for (const [statementIndex, statement] of statements.entries()) {
613
+ try {
614
+ const { affectedRows, executionDurationMs } = await executeInsertStatement(targetConnection, statement);
615
+ insertedRows += affectedRows;
616
+ adjustRemoteBatchTuning(targetConnection, affectedRows, executionDurationMs, getSqlByteLength(statement.sql));
617
+ }
618
+ catch (error) {
619
+ const remainingGroups = statements
620
+ .slice(statementIndex + 1)
621
+ .map((remainingStatement) => remainingStatement.rows)
622
+ .filter((remainingRows) => remainingRows.length > 0);
623
+ const nextRowsPerStatement = reduceRemoteBatchTuningAfterFailure(targetConnection);
624
+ if (nextRowsPerStatement <= 0 || statement.rows.length <= 1) {
625
+ const innerReason = getErrorMessage(error);
626
+ throw ErrorFactory.createValidationError(`Insert failed for table ${tableName}: ${innerReason}`, {
627
+ rowCount: statement.rowCount,
628
+ reason: innerReason,
629
+ });
630
+ }
631
+ pendingRows.unshift(statement.rows, ...remainingGroups);
632
+ shouldRetryCurrentRows = true;
633
+ break;
634
+ }
635
+ }
636
+ if (shouldRetryCurrentRows) {
637
+ continue;
580
638
  }
581
- sqlParts.push(statement.sql);
582
- parameters.push(...statement.parameters);
583
- rowCount += statement.rowCount;
584
- currentLength += (sqlParts.length > 1 ? 2 : 0) + statement.sql.length;
585
639
  }
586
- flushBatch();
587
- return batches;
640
+ return insertedRows;
588
641
  };
589
642
  const createRemoteD1Adapter = (database) => {
590
643
  return {
@@ -1012,6 +1065,8 @@ export const DataMigrator = Object.freeze({
1012
1065
  try {
1013
1066
  const totalRows = table.rowCount || 0;
1014
1067
  const batchSize = config.batchSize || 1000;
1068
+ resetSourceBatchTuning(sourceConnection, batchSize);
1069
+ resetRemoteBatchTuning(targetConnection);
1015
1070
  const targetRowCount = await DataMigrator.getTargetRowCount(targetConnection, table.name);
1016
1071
  if (targetRowCount >= totalRows) {
1017
1072
  Logger.info(`Table ${table.name} already synced: ${targetRowCount}/${totalRows} rows, skipping`);
@@ -1024,6 +1079,16 @@ export const DataMigrator = Object.freeze({
1024
1079
  Logger.info(`Processing ${totalRows} rows in batches of ${batchSize}`);
1025
1080
  }
1026
1081
  rowsMigrated = await DataMigrator.processTableChunks(table, sourceConnection, targetConnection, totalRows, batchSize, targetRowCount, errors);
1082
+ if (errors.length > 0 && rowsMigrated < totalRows) {
1083
+ appendFailedTableReport({
1084
+ migrationId: config.migrationId || 'unknown',
1085
+ targetDatabase: targetConnection.database,
1086
+ tableName: table.name,
1087
+ rowsMigrated,
1088
+ totalRows,
1089
+ errors,
1090
+ });
1091
+ }
1027
1092
  const tableDurationMs = Date.now() - tableStartTime;
1028
1093
  Logger.info(`[DataMigrator] Table ${table.name} completed rows=${rowsMigrated}/${totalRows} duration=${formatDuration(tableDurationMs)} rate=${formatRowsPerSecond(rowsMigrated, tableDurationMs)}`);
1029
1094
  return { rowsMigrated, errors };
@@ -1032,6 +1097,14 @@ export const DataMigrator = Object.freeze({
1032
1097
  const errorMsg = `Table migration failed for ${table.name}: ${error}`;
1033
1098
  Logger.error(errorMsg);
1034
1099
  errors.push(errorMsg);
1100
+ appendFailedTableReport({
1101
+ migrationId: config.migrationId || 'unknown',
1102
+ targetDatabase: targetConnection.database,
1103
+ tableName: table.name,
1104
+ rowsMigrated,
1105
+ totalRows: table.rowCount || 0,
1106
+ errors,
1107
+ });
1035
1108
  return { rowsMigrated, errors };
1036
1109
  }
1037
1110
  },
@@ -1046,8 +1119,7 @@ export const DataMigrator = Object.freeze({
1046
1119
  state.currentBatchSize = getTableChunkBatchSize(sourceConnection, state.retryBatchSize);
1047
1120
  const result = await processTableChunk(table, sourceConnection, targetConnection, state.offset, state.currentBatchSize, errors);
1048
1121
  state = applyTableChunkResult(state, result, batchSize, table, totalRows);
1049
- if (!result.continueProcessing &&
1050
- !(result.rowsMigrated === 0 && result.nextBatchSize < batchSize)) {
1122
+ if (!result.continueProcessing) {
1051
1123
  break;
1052
1124
  }
1053
1125
  }
@@ -1121,33 +1193,17 @@ export const DataMigrator = Object.freeze({
1121
1193
  if (!targetConnection.adapter) {
1122
1194
  throw ErrorFactory.createValidationError(`No target adapter configured for ${targetConnection.database}`);
1123
1195
  }
1196
+ if (targetConnection.type === 'd1-remote') {
1197
+ return insertRemoteRowsWithRetry(targetConnection, tableName, data);
1198
+ }
1124
1199
  const batchSettings = getInsertBatchSettings(targetConnection);
1125
1200
  const statements = createInsertStatements(targetConnection.type, batchSettings, tableName, data);
1126
- const executableStatements = targetConnection.type === 'd1-remote'
1127
- ? createRemoteExecutionBatchesWithLimit(statements, batchSettings.maxExecutionSqlLength)
1128
- : statements;
1201
+ const executableStatements = statements;
1129
1202
  let insertedRows = 0;
1130
1203
  for (const statement of executableStatements) {
1131
- try {
1132
- const executionStartTime = Date.now();
1133
- const result = await targetConnection.adapter.query(statement.sql, statement.parameters);
1134
- const executionDurationMs = Date.now() - executionStartTime;
1135
- const affectedRows = typeof result.rowCount === 'number' ? result.rowCount : statement.rowCount;
1136
- insertedRows += affectedRows;
1137
- adjustRemoteBatchTuning(targetConnection, affectedRows, executionDurationMs, statement.sql.length);
1138
- }
1139
- catch {
1140
- if (targetConnection.type === 'd1-remote' && data.length > 1 && statement.rowCount > 1) {
1141
- reduceRemoteBatchTuningAfterFailure(targetConnection, statement.sql.length);
1142
- const midpoint = Math.max(1, Math.floor(data.length / 2));
1143
- const left = await DataMigrator.insertData(targetConnection, tableName, data.slice(0, midpoint));
1144
- const right = await DataMigrator.insertData(targetConnection, tableName, data.slice(midpoint));
1145
- return left + right;
1146
- }
1147
- throw ErrorFactory.createValidationError(`Insert failed for table ${tableName}`, {
1148
- rowCount: statement.rowCount,
1149
- });
1150
- }
1204
+ const { affectedRows, executionDurationMs } = await executeInsertStatement(targetConnection, statement);
1205
+ insertedRows += affectedRows;
1206
+ adjustRemoteBatchTuning(targetConnection, affectedRows, executionDurationMs, getSqlByteLength(statement.sql));
1151
1207
  }
1152
1208
  return insertedRows;
1153
1209
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/d1-migrator",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Resumable database migration toolkit for moving data to Cloudflare D1 with ZinTrust.",
5
5
  "private": false,
6
6
  "type": "module",