@zintrust/d1-migrator 2.0.2 → 2.0.4

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 CHANGED
@@ -226,6 +226,9 @@ The command supports env-based execution for all CLI settings.
226
226
  | Interactive (`--interactive`) | `MIGRATE_TO_D1_INTERACTIVE`, `D1_MIGRATOR_INTERACTIVE` |
227
227
  | Resume (`--resume`) | `MIGRATE_TO_D1_RESUME`, `D1_MIGRATOR_RESUME` |
228
228
  | Migration ID (`--migration-id`) | `MIGRATE_TO_D1_MIGRATION_ID`, `D1_MIGRATOR_MIGRATION_ID` |
229
+ | Group Small Tables | `MIGRATE_TO_D1_GROUP_SMALL_TABLES` (Default: `true`) |
230
+ | Max Group Rows | `MIGRATE_TO_D1_MAX_GROUP_ROWS` (Default: `50000`) |
231
+ | Max Group Size (MB) | `MIGRATE_TO_D1_MAX_GROUP_SIZE_MB` (Default: `10`) |
229
232
 
230
233
  If `--source-connection` is not provided, the command automatically composes a URI from `DB_*` values for MySQL/PostgreSQL/SQL Server, and uses `DB_PATH`/`DB_DATABASE` for SQLite. Host fallback prefers `DB_READ_HOSTS`, then `DB_HOSTS`, then `DB_HOST`.
231
234
 
@@ -30,6 +30,7 @@ export interface TargetConnection {
30
30
  connected: boolean;
31
31
  adapter?: DatabaseAdapter;
32
32
  remoteBatchTuning?: RemoteBatchTuning;
33
+ targetRowCountsCache?: Record<string, number>;
33
34
  }
34
35
  export interface TableInfo {
35
36
  name: string;
@@ -83,6 +84,10 @@ export declare const DataMigrator: Readonly<{
83
84
  getSchemaInfo(_connection: SourceConnection): Promise<{
84
85
  tables: TableInfo[];
85
86
  }>;
87
+ /**
88
+ * Populate target table row counts cache to optimize performance and prevent countless individual DB count queries
89
+ */
90
+ populateTargetRowCountsCache(targetConnection: TargetConnection, tables: TableInfo[]): Promise<void>;
86
91
  /**
87
92
  * Get target table row count for resumability
88
93
  */
@@ -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":"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"}
@@ -4,7 +4,7 @@
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';
7
+ import { appendFileSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
8
8
  import * as path from 'node:path';
9
9
  import { MySQLAdapter } from '@zintrust/db-mysql';
10
10
  import { PostgreSQLAdapter } from '@zintrust/db-postgres';
@@ -147,6 +147,16 @@ const SOURCE_CONNECT_RETRY_ATTEMPTS = 3;
147
147
  const SOURCE_CONNECT_RETRY_BASE_DELAY_MS = 500;
148
148
  const FAILED_TABLE_REPORT_PATH = path.join('logs', 'd1-migration-failed-report.log');
149
149
  const REMOTE_SQL_GROW_THRESHOLD_RATIO = 0.6;
150
+ const getEnvBatchSize = () => {
151
+ const envVal = process.env['MIGRATE_TO_D1_BATCH_SIZE'] || process.env['D1_MIGRATOR_BATCH_SIZE'];
152
+ if (envVal) {
153
+ const parsed = Number.parseInt(envVal, 10);
154
+ if (!Number.isNaN(parsed) && parsed > 0) {
155
+ return parsed;
156
+ }
157
+ }
158
+ return undefined;
159
+ };
150
160
  const formatDuration = (durationMs) => {
151
161
  if (durationMs < 1000) {
152
162
  return `${durationMs}ms`;
@@ -180,14 +190,16 @@ const normalizeSourceBatchSize = (requestedBatchSize) => {
180
190
  return Math.max(MIN_SOURCE_READ_BATCH_SIZE, requestedBatchSize);
181
191
  };
182
192
  const createSourceBatchTuning = (initialBatchSize) => {
193
+ const finalSize = getEnvBatchSize() ?? initialBatchSize;
183
194
  return {
184
- readBatchSize: normalizeSourceBatchSize(initialBatchSize),
195
+ readBatchSize: normalizeSourceBatchSize(finalSize),
185
196
  readBatchHistory: [],
186
197
  };
187
198
  };
188
- const createRemoteBatchTuning = () => {
199
+ const createRemoteBatchTuning = (initialBatchSize) => {
200
+ const finalSize = initialBatchSize ?? getEnvBatchSize() ?? REMOTE_INSERT_ROWS_PER_STATEMENT;
189
201
  return {
190
- rowsPerStatement: REMOTE_INSERT_ROWS_PER_STATEMENT,
202
+ rowsPerStatement: Math.max(MIN_REMOTE_INSERT_ROWS_PER_STATEMENT, finalSize),
191
203
  maxStatementSqlLength: MAX_REMOTE_INSERT_SQL_LENGTH,
192
204
  maxExecutionSqlLength: MAX_REMOTE_EXECUTION_SQL_LENGTH,
193
205
  };
@@ -277,18 +289,18 @@ const reduceSourceBatchSizeAfterTimeout = (connection, batchSize) => {
277
289
  Logger.warn(`[DataMigrator] Source batch retry: ${currentSize} -> ${reducedSize} rows after retryable read failure`);
278
290
  return reducedSize;
279
291
  };
280
- const getRemoteBatchTuning = (connection) => {
292
+ const getRemoteBatchTuning = (connection, initialBatchSize) => {
281
293
  if (connection.remoteBatchTuning !== undefined) {
282
294
  return connection.remoteBatchTuning;
283
295
  }
284
- connection.remoteBatchTuning = createRemoteBatchTuning();
296
+ connection.remoteBatchTuning = createRemoteBatchTuning(initialBatchSize);
285
297
  return connection.remoteBatchTuning;
286
298
  };
287
- const resetRemoteBatchTuning = (connection) => {
299
+ const resetRemoteBatchTuning = (connection, initialBatchSize) => {
288
300
  if (connection.type !== 'd1-remote') {
289
301
  return;
290
302
  }
291
- connection.remoteBatchTuning = createRemoteBatchTuning();
303
+ connection.remoteBatchTuning = createRemoteBatchTuning(initialBatchSize);
292
304
  };
293
305
  const getInsertBatchSettings = (connection) => {
294
306
  if (connection.type === 'd1-remote') {
@@ -507,6 +519,153 @@ const buildTableMigrationLevels = (tables) => {
507
519
  }
508
520
  return levels;
509
521
  };
522
+ function groupTablesByLimits(tables) {
523
+ const isGroupingEnabled = (process.env['MIGRATE_TO_D1_GROUP_SMALL_TABLES'] ?? 'true') === 'true';
524
+ if (!isGroupingEnabled) {
525
+ return { largeTables: tables, groups: [] };
526
+ }
527
+ const maxGroupRows = Number.parseInt(process.env['MIGRATE_TO_D1_MAX_GROUP_ROWS'] ?? '50000', 10);
528
+ const maxGroupSizeBytes = Number.parseInt(process.env['MIGRATE_TO_D1_MAX_GROUP_SIZE_MB'] ?? '10', 10) * 1024 * 1024;
529
+ const largeTables = [];
530
+ const groups = [];
531
+ let currentGroup = [];
532
+ let currentGroupRows = 0;
533
+ let currentGroupBytes = 0;
534
+ for (const table of tables) {
535
+ const rowCount = table.rowCount || 0;
536
+ const estimatedBytes = rowCount * 400;
537
+ if (rowCount > maxGroupRows || estimatedBytes > maxGroupSizeBytes) {
538
+ largeTables.push(table);
539
+ continue;
540
+ }
541
+ const exceedsRows = currentGroupRows + rowCount > maxGroupRows;
542
+ const exceedsBytes = currentGroupBytes + estimatedBytes > maxGroupSizeBytes;
543
+ if (exceedsRows || exceedsBytes) {
544
+ if (currentGroup.length > 0) {
545
+ groups.push(currentGroup);
546
+ }
547
+ currentGroup = [table];
548
+ currentGroupRows = rowCount;
549
+ currentGroupBytes = estimatedBytes;
550
+ }
551
+ else {
552
+ currentGroup.push(table);
553
+ currentGroupRows += rowCount;
554
+ currentGroupBytes += estimatedBytes;
555
+ }
556
+ }
557
+ if (currentGroup.length > 0) {
558
+ groups.push(currentGroup);
559
+ }
560
+ const finalGroups = groups.filter((group) => {
561
+ if (group.length === 1 && group[0] !== undefined) {
562
+ largeTables.push(group[0]);
563
+ return false;
564
+ }
565
+ return true;
566
+ });
567
+ return { largeTables, groups: finalGroups };
568
+ }
569
+ const processSmallTableRows = async (table, sourceConnection, targetConnection, rowsMigratedMap, allSqlStatements) => {
570
+ const totalRows = table.rowCount || 0;
571
+ const targetRowCount = await DataMigrator.getTargetRowCount(targetConnection, table.name);
572
+ if (targetRowCount >= totalRows) {
573
+ Logger.info(`[DataMigrator] Table ${table.name} already synced: ${targetRowCount}/${totalRows} rows, skipping in group`);
574
+ rowsMigratedMap[table.name] = totalRows;
575
+ return;
576
+ }
577
+ const rows = await DataMigrator.readDataChunk(sourceConnection, table.name, 0, totalRows);
578
+ if (rows.length === 0) {
579
+ rowsMigratedMap[table.name] = 0;
580
+ return;
581
+ }
582
+ const transformedRows = await DataMigrator.transformData(rows, table.name);
583
+ const batchSettings = getInsertBatchSettings(targetConnection);
584
+ const statements = createInsertStatements(targetConnection.type, batchSettings, table.name, transformedRows);
585
+ for (const statement of statements) {
586
+ allSqlStatements.push(bindSqlParameters(statement.sql, statement.parameters));
587
+ }
588
+ rowsMigratedMap[table.name] = rows.length;
589
+ };
590
+ async function migrateSmallTablesGroup(tables, sourceConnection, targetConnection, config) {
591
+ Logger.info(`[DataMigrator] Group migrating ${tables.length} small tables: ${tables.map((t) => t.name).join(', ')}`);
592
+ const errors = [];
593
+ const rowsMigratedMap = {};
594
+ const tempFileDir = path.join('.wrangler', 'tmp');
595
+ const tempFileName = `migration-group-${Date.now()}-${Math.random().toString(36).substring(7)}.sql`;
596
+ const tempFilePath = path.join(tempFileDir, tempFileName);
597
+ try {
598
+ mkdirSync(tempFileDir, { recursive: true });
599
+ const allSqlStatements = [];
600
+ for (const table of tables) {
601
+ await processSmallTableRows(table, sourceConnection, targetConnection, rowsMigratedMap, allSqlStatements);
602
+ }
603
+ if (allSqlStatements.length > 0) {
604
+ const sqlContent = allSqlStatements.join(';\n') + ';';
605
+ writeFileSync(tempFilePath, sqlContent, 'utf8');
606
+ const executionStartTime = Date.now();
607
+ WranglerD1.executeSql({
608
+ dbName: targetConnection.database,
609
+ isLocal: targetConnection.type !== 'd1-remote',
610
+ file: tempFilePath,
611
+ });
612
+ const durationMs = Date.now() - executionStartTime;
613
+ if (targetConnection.targetRowCountsCache) {
614
+ for (const table of tables) {
615
+ targetConnection.targetRowCountsCache[table.name] = table.rowCount || 0;
616
+ }
617
+ }
618
+ Logger.info(`[DataMigrator] Successfully migrated group of ${tables.length} tables in ${formatDuration(durationMs)}`);
619
+ }
620
+ try {
621
+ unlinkSync(tempFilePath);
622
+ }
623
+ catch {
624
+ // Ignore
625
+ }
626
+ return { rowsMigratedMap, errors };
627
+ }
628
+ catch (error) {
629
+ try {
630
+ unlinkSync(tempFilePath);
631
+ }
632
+ catch {
633
+ // Ignore
634
+ }
635
+ const errMsg = `Group migration failed: ${getErrorMessage(error)}`;
636
+ Logger.warn(`[DataMigrator] ${errMsg}. Falling back to individual table migration...`);
637
+ for (const table of tables) {
638
+ const result = await DataMigrator.migrateTable(table, sourceConnection, targetConnection, config);
639
+ rowsMigratedMap[table.name] = result.rowsMigrated;
640
+ errors.push(...result.errors);
641
+ }
642
+ return { rowsMigratedMap, errors };
643
+ }
644
+ }
645
+ const migrateTableLevel = async (levelIndex, tables, tableLevels, tableParallelism, sourceConnection, targetConnection, config, progress) => {
646
+ Logger.info(`[DataMigrator] Starting table level ${levelIndex + 1}/${tableLevels.length}: ${tables.map((table) => table.name).join(', ')}`);
647
+ const { largeTables, groups } = groupTablesByLimits(tables);
648
+ const levelResults = await executeWithConcurrency(largeTables, tableParallelism, async (table) => {
649
+ return DataMigrator.migrateTable(table, sourceConnection, targetConnection, config);
650
+ });
651
+ for (const [resultIndex, result] of levelResults.entries()) {
652
+ const table = largeTables[resultIndex];
653
+ progress.processedRows += result.rowsMigrated;
654
+ if (result.errors.length > 0 && table !== undefined) {
655
+ progress.errors[table.name] = result.errors.join('; ');
656
+ }
657
+ }
658
+ for (const group of groups) {
659
+ const groupResult = await migrateSmallTablesGroup(group, sourceConnection, targetConnection, config);
660
+ for (const table of group) {
661
+ const rowsMigrated = groupResult.rowsMigratedMap[table.name] ?? 0;
662
+ progress.processedRows += rowsMigrated;
663
+ if (groupResult.errors.length > 0) {
664
+ progress.errors[table.name] = groupResult.errors.join('; ');
665
+ }
666
+ }
667
+ }
668
+ };
510
669
  const getTableParallelism = (config, targetConnection) => {
511
670
  if (targetConnection.type !== 'd1-remote') {
512
671
  return 1;
@@ -599,7 +758,42 @@ const createRemoteInsertExecutionQueue = (targetConnection, tableName, rows) =>
599
758
  const batchSettings = getInsertBatchSettings(targetConnection);
600
759
  return createInsertStatements(targetConnection.type, batchSettings, tableName, rows);
601
760
  };
602
- const insertRemoteRowsWithRetry = async (targetConnection, tableName, data) => {
761
+ const executeRemoteFileBatch = async (targetConnection, tableName, statements) => {
762
+ const tempFileDir = path.join('.wrangler', 'tmp');
763
+ const tempFileName = `migration-${tableName}-${Date.now()}-${Math.random().toString(36).substring(7)}.sql`;
764
+ const tempFilePath = path.join(tempFileDir, tempFileName);
765
+ try {
766
+ mkdirSync(tempFileDir, { recursive: true });
767
+ const sqlContent = statements.map((s) => bindSqlParameters(s.sql, s.parameters)).join(';\n') + ';';
768
+ writeFileSync(tempFilePath, sqlContent, 'utf8');
769
+ const executionStartTime = Date.now();
770
+ WranglerD1.executeSql({
771
+ dbName: targetConnection.database,
772
+ isLocal: false,
773
+ file: tempFilePath,
774
+ });
775
+ const durationMs = Date.now() - executionStartTime;
776
+ try {
777
+ unlinkSync(tempFilePath);
778
+ }
779
+ catch {
780
+ // Ignore deletion error
781
+ }
782
+ const affectedRows = statements.reduce((sum, s) => sum + s.rowCount, 0);
783
+ return { affectedRows, durationMs, sqlLength: getSqlByteLength(sqlContent) };
784
+ }
785
+ catch (error) {
786
+ try {
787
+ unlinkSync(tempFilePath);
788
+ }
789
+ catch {
790
+ // Ignore deletion error
791
+ }
792
+ Logger.warn(`[DataMigrator] High-speed SQL file insert failed for ${tableName}, falling back to adaptive statement-by-statement execution. Error: ${getErrorMessage(error)}`);
793
+ return null;
794
+ }
795
+ };
796
+ async function insertRemoteRowsWithRetry(targetConnection, tableName, data) {
603
797
  const pendingRows = [data];
604
798
  let insertedRows = 0;
605
799
  while (pendingRows.length > 0) {
@@ -609,6 +803,14 @@ const insertRemoteRowsWithRetry = async (targetConnection, tableName, data) => {
609
803
  }
610
804
  const statements = createRemoteInsertExecutionQueue(targetConnection, tableName, currentRows);
611
805
  let shouldRetryCurrentRows = false;
806
+ if (targetConnection.type === 'd1-remote' && statements.length > 0) {
807
+ const fileResult = await executeRemoteFileBatch(targetConnection, tableName, statements);
808
+ if (fileResult !== null) {
809
+ insertedRows += fileResult.affectedRows;
810
+ adjustRemoteBatchTuning(targetConnection, fileResult.affectedRows, fileResult.durationMs, fileResult.sqlLength);
811
+ continue;
812
+ }
813
+ }
612
814
  for (const [statementIndex, statement] of statements.entries()) {
613
815
  try {
614
816
  const { affectedRows, executionDurationMs } = await executeInsertStatement(targetConnection, statement);
@@ -638,7 +840,7 @@ const insertRemoteRowsWithRetry = async (targetConnection, tableName, data) => {
638
840
  }
639
841
  }
640
842
  return insertedRows;
641
- };
843
+ }
642
844
  const createRemoteD1Adapter = (database) => {
643
845
  return {
644
846
  async connect() {
@@ -676,7 +878,21 @@ const getErrorCause = (error) => {
676
878
  };
677
879
  const getErrorMessage = (error) => {
678
880
  if (error instanceof Error) {
679
- return error.message;
881
+ let msg = error.message;
882
+ const errObj = error;
883
+ if (errObj.stderr && (typeof errObj.stderr === 'string' || Buffer.isBuffer(errObj.stderr))) {
884
+ const stderrStr = errObj.stderr.toString().trim();
885
+ if (stderrStr) {
886
+ msg += `\nStderr: ${stderrStr}`;
887
+ }
888
+ }
889
+ if (errObj.stdout && (typeof errObj.stdout === 'string' || Buffer.isBuffer(errObj.stdout))) {
890
+ const stdoutStr = errObj.stdout.toString().trim();
891
+ if (stdoutStr) {
892
+ msg += `\nStdout: ${stdoutStr}`;
893
+ }
894
+ }
895
+ return msg;
680
896
  }
681
897
  return String(error);
682
898
  };
@@ -692,6 +908,61 @@ const getErrorChainMessages = (error) => {
692
908
  }
693
909
  return [...new Set(messages)];
694
910
  };
911
+ const cacheTargetRowCount = (targetConnection, row) => {
912
+ const name = typeof row['name'] === 'string' ? row['name'] : null;
913
+ const count = Number(row['count']);
914
+ if (name !== null && Number.isFinite(count) && targetConnection.targetRowCountsCache) {
915
+ targetConnection.targetRowCountsCache[name] = count;
916
+ }
917
+ };
918
+ const cacheTargetRowCountPayload = (targetConnection, payload) => {
919
+ for (const statementResult of payload) {
920
+ const row = statementResult.results?.[0];
921
+ if (row !== undefined) {
922
+ cacheTargetRowCount(targetConnection, row);
923
+ }
924
+ }
925
+ };
926
+ const unlinkTempFile = (tempFilePath) => {
927
+ try {
928
+ unlinkSync(tempFilePath);
929
+ }
930
+ catch (error) {
931
+ Logger.debug(`[DataMigrator] Failed to remove temp file ${tempFilePath}: ${getErrorMessage(error)}`);
932
+ }
933
+ };
934
+ const executeRemoteCountPrefetch = (targetConnection, sqlStatements) => {
935
+ const tempFileDir = path.join('.wrangler', 'tmp');
936
+ const tempFileName = `migration-counts-${Date.now()}-${Math.random().toString(36).substring(7)}.sql`;
937
+ const tempFilePath = path.join(tempFileDir, tempFileName);
938
+ try {
939
+ mkdirSync(tempFileDir, { recursive: true });
940
+ writeFileSync(tempFilePath, `${sqlStatements};`, 'utf8');
941
+ const output = WranglerD1.executeSql({
942
+ dbName: targetConnection.database,
943
+ isLocal: false,
944
+ file: tempFilePath,
945
+ });
946
+ const payload = extractWranglerJson(output);
947
+ if (payload !== null) {
948
+ cacheTargetRowCountPayload(targetConnection, payload);
949
+ }
950
+ }
951
+ finally {
952
+ unlinkTempFile(tempFilePath);
953
+ }
954
+ };
955
+ const executeLocalCountPrefetch = async (targetConnection, sqlStatements) => {
956
+ if (!targetConnection.adapter) {
957
+ return;
958
+ }
959
+ for (const sql of sqlStatements) {
960
+ const result = await targetConnection.adapter.query(sql, []);
961
+ for (const row of result.rows) {
962
+ cacheTargetRowCount(targetConnection, row);
963
+ }
964
+ }
965
+ };
695
966
  const describeDriverError = (error) => {
696
967
  if (error === null || typeof error !== 'object') {
697
968
  return undefined;
@@ -858,22 +1129,13 @@ export const DataMigrator = Object.freeze({
858
1129
  Logger.info(`Migrating ${progress.totalTables} tables with ${progress.totalRows} total rows`);
859
1130
  if (targetConnection.adapter) {
860
1131
  await DataMigrator.prepareTargetSchema(sourceConnection, targetConnection, config);
1132
+ await DataMigrator.populateTargetRowCountsCache(targetConnection, schema.tables);
861
1133
  }
862
1134
  Logger.info('Starting table migration...');
863
1135
  const tableLevels = buildTableMigrationLevels(schema.tables);
864
1136
  const tableParallelism = getTableParallelism(config, targetConnection);
865
1137
  for (const [levelIndex, tables] of tableLevels.entries()) {
866
- Logger.info(`[DataMigrator] Starting table level ${levelIndex + 1}/${tableLevels.length}: ${tables.map((table) => table.name).join(', ')}`);
867
- const levelResults = await executeWithConcurrency(tables, tableParallelism, async (table) => {
868
- return DataMigrator.migrateTable(table, sourceConnection, targetConnection, config);
869
- });
870
- for (const [resultIndex, result] of levelResults.entries()) {
871
- const table = tables[resultIndex];
872
- progress.processedRows += result.rowsMigrated;
873
- if (result.errors.length > 0 && table !== undefined) {
874
- progress.errors[table.name] = result.errors.join('; ');
875
- }
876
- }
1138
+ await migrateTableLevel(levelIndex, tables, tableLevels, tableParallelism, sourceConnection, targetConnection, config, progress);
877
1139
  }
878
1140
  progress.totalRows = Math.max(progress.totalRows, progress.processedRows);
879
1141
  // Update final percentage
@@ -986,8 +1248,15 @@ export const DataMigrator = Object.freeze({
986
1248
  if (schemaStatements.length === 0) {
987
1249
  return;
988
1250
  }
989
- const batchSql = `${schemaStatements.join(';\n')};`;
990
- await adapter.query(batchSql, []);
1251
+ if (targetConnection.type === 'd1-remote') {
1252
+ const batchSql = `${schemaStatements.join(';\n')};`;
1253
+ await adapter.query(batchSql, []);
1254
+ }
1255
+ else {
1256
+ for (const statement of schemaStatements) {
1257
+ await adapter.query(statement, []);
1258
+ }
1259
+ }
991
1260
  schemaStatements.length = 0;
992
1261
  };
993
1262
  const pushSchemaStatement = async (sql) => {
@@ -1038,12 +1307,43 @@ export const DataMigrator = Object.freeze({
1038
1307
  Logger.info(`Found ${tables.length} tables`);
1039
1308
  return { tables };
1040
1309
  },
1310
+ /**
1311
+ * Populate target table row counts cache to optimize performance and prevent countless individual DB count queries
1312
+ */
1313
+ async populateTargetRowCountsCache(targetConnection, tables) {
1314
+ if (!targetConnection.adapter)
1315
+ return;
1316
+ targetConnection.targetRowCountsCache = {};
1317
+ if (tables.length === 0)
1318
+ return;
1319
+ Logger.info(`[DataMigrator] Pre-fetching target row counts for ${tables.length} tables to optimize migration speed...`);
1320
+ const chunkSize = 50;
1321
+ for (let i = 0; i < tables.length; i += chunkSize) {
1322
+ const chunk = tables.slice(i, i + chunkSize);
1323
+ const countQueries = chunk.map((t) => `SELECT '${t.name.replaceAll("'", "''")}' as name, COUNT(*) as count FROM \`${t.name}\``);
1324
+ try {
1325
+ if (targetConnection.type === 'd1-remote') {
1326
+ executeRemoteCountPrefetch(targetConnection, countQueries.join(';\n'));
1327
+ }
1328
+ else {
1329
+ await executeLocalCountPrefetch(targetConnection, countQueries);
1330
+ }
1331
+ }
1332
+ catch (error) {
1333
+ Logger.debug(`[DataMigrator] Chunked pre-fetch failed, falling back to individual table counts on demand. Error: ${getErrorMessage(error)}`);
1334
+ }
1335
+ }
1336
+ },
1041
1337
  /**
1042
1338
  * Get target table row count for resumability
1043
1339
  */
1044
1340
  async getTargetRowCount(targetConnection, tableName) {
1045
1341
  if (!targetConnection.adapter)
1046
1342
  return 0;
1343
+ if (targetConnection.targetRowCountsCache &&
1344
+ typeof targetConnection.targetRowCountsCache[tableName] === 'number') {
1345
+ return targetConnection.targetRowCountsCache[tableName];
1346
+ }
1047
1347
  try {
1048
1348
  const result = await targetConnection.adapter.query(`SELECT COUNT(*) as count FROM \`${tableName}\``, []);
1049
1349
  const count = result.rows[0]?.['count'];
@@ -1066,7 +1366,7 @@ export const DataMigrator = Object.freeze({
1066
1366
  const totalRows = table.rowCount || 0;
1067
1367
  const batchSize = config.batchSize || 1000;
1068
1368
  resetSourceBatchTuning(sourceConnection, batchSize);
1069
- resetRemoteBatchTuning(targetConnection);
1369
+ resetRemoteBatchTuning(targetConnection, batchSize);
1070
1370
  const targetRowCount = await DataMigrator.getTargetRowCount(targetConnection, table.name);
1071
1371
  if (targetRowCount >= totalRows) {
1072
1372
  Logger.info(`Table ${table.name} already synced: ${targetRowCount}/${totalRows} rows, skipping`);
@@ -1194,7 +1494,12 @@ export const DataMigrator = Object.freeze({
1194
1494
  throw ErrorFactory.createValidationError(`No target adapter configured for ${targetConnection.database}`);
1195
1495
  }
1196
1496
  if (targetConnection.type === 'd1-remote') {
1197
- return insertRemoteRowsWithRetry(targetConnection, tableName, data);
1497
+ const insertedRows = await insertRemoteRowsWithRetry(targetConnection, tableName, data);
1498
+ if (targetConnection.targetRowCountsCache &&
1499
+ typeof targetConnection.targetRowCountsCache[tableName] === 'number') {
1500
+ targetConnection.targetRowCountsCache[tableName] += insertedRows;
1501
+ }
1502
+ return insertedRows;
1198
1503
  }
1199
1504
  const batchSettings = getInsertBatchSettings(targetConnection);
1200
1505
  const statements = createInsertStatements(targetConnection.type, batchSettings, tableName, data);
@@ -1205,6 +1510,10 @@ export const DataMigrator = Object.freeze({
1205
1510
  insertedRows += affectedRows;
1206
1511
  adjustRemoteBatchTuning(targetConnection, affectedRows, executionDurationMs, getSqlByteLength(statement.sql));
1207
1512
  }
1513
+ if (targetConnection.targetRowCountsCache &&
1514
+ typeof targetConnection.targetRowCountsCache[tableName] === 'number') {
1515
+ targetConnection.targetRowCountsCache[tableName] += insertedRows;
1516
+ }
1208
1517
  return insertedRows;
1209
1518
  },
1210
1519
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/d1-migrator",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Resumable database migration toolkit for moving data to Cloudflare D1 with ZinTrust.",
5
5
  "private": false,
6
6
  "type": "module",