@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 +3 -0
- package/dist/cli/DataMigrator.d.ts +5 -0
- package/dist/cli/DataMigrator.d.ts.map +1 -1
- package/dist/cli/DataMigrator.js +335 -26
- package/package.json +1 -1
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":"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;
|
|
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
|
@@ -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(
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
990
|
-
|
|
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
|
-
|
|
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
|
/**
|