@zintrust/d1-migrator 2.0.3 → 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 +315 -18
- 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';
|
|
@@ -519,6 +519,153 @@ const buildTableMigrationLevels = (tables) => {
|
|
|
519
519
|
}
|
|
520
520
|
return levels;
|
|
521
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
|
+
};
|
|
522
669
|
const getTableParallelism = (config, targetConnection) => {
|
|
523
670
|
if (targetConnection.type !== 'd1-remote') {
|
|
524
671
|
return 1;
|
|
@@ -611,7 +758,42 @@ const createRemoteInsertExecutionQueue = (targetConnection, tableName, rows) =>
|
|
|
611
758
|
const batchSettings = getInsertBatchSettings(targetConnection);
|
|
612
759
|
return createInsertStatements(targetConnection.type, batchSettings, tableName, rows);
|
|
613
760
|
};
|
|
614
|
-
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) {
|
|
615
797
|
const pendingRows = [data];
|
|
616
798
|
let insertedRows = 0;
|
|
617
799
|
while (pendingRows.length > 0) {
|
|
@@ -621,6 +803,14 @@ const insertRemoteRowsWithRetry = async (targetConnection, tableName, data) => {
|
|
|
621
803
|
}
|
|
622
804
|
const statements = createRemoteInsertExecutionQueue(targetConnection, tableName, currentRows);
|
|
623
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
|
+
}
|
|
624
814
|
for (const [statementIndex, statement] of statements.entries()) {
|
|
625
815
|
try {
|
|
626
816
|
const { affectedRows, executionDurationMs } = await executeInsertStatement(targetConnection, statement);
|
|
@@ -650,7 +840,7 @@ const insertRemoteRowsWithRetry = async (targetConnection, tableName, data) => {
|
|
|
650
840
|
}
|
|
651
841
|
}
|
|
652
842
|
return insertedRows;
|
|
653
|
-
}
|
|
843
|
+
}
|
|
654
844
|
const createRemoteD1Adapter = (database) => {
|
|
655
845
|
return {
|
|
656
846
|
async connect() {
|
|
@@ -688,7 +878,21 @@ const getErrorCause = (error) => {
|
|
|
688
878
|
};
|
|
689
879
|
const getErrorMessage = (error) => {
|
|
690
880
|
if (error instanceof Error) {
|
|
691
|
-
|
|
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;
|
|
692
896
|
}
|
|
693
897
|
return String(error);
|
|
694
898
|
};
|
|
@@ -704,6 +908,61 @@ const getErrorChainMessages = (error) => {
|
|
|
704
908
|
}
|
|
705
909
|
return [...new Set(messages)];
|
|
706
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
|
+
};
|
|
707
966
|
const describeDriverError = (error) => {
|
|
708
967
|
if (error === null || typeof error !== 'object') {
|
|
709
968
|
return undefined;
|
|
@@ -870,22 +1129,13 @@ export const DataMigrator = Object.freeze({
|
|
|
870
1129
|
Logger.info(`Migrating ${progress.totalTables} tables with ${progress.totalRows} total rows`);
|
|
871
1130
|
if (targetConnection.adapter) {
|
|
872
1131
|
await DataMigrator.prepareTargetSchema(sourceConnection, targetConnection, config);
|
|
1132
|
+
await DataMigrator.populateTargetRowCountsCache(targetConnection, schema.tables);
|
|
873
1133
|
}
|
|
874
1134
|
Logger.info('Starting table migration...');
|
|
875
1135
|
const tableLevels = buildTableMigrationLevels(schema.tables);
|
|
876
1136
|
const tableParallelism = getTableParallelism(config, targetConnection);
|
|
877
1137
|
for (const [levelIndex, tables] of tableLevels.entries()) {
|
|
878
|
-
|
|
879
|
-
const levelResults = await executeWithConcurrency(tables, tableParallelism, async (table) => {
|
|
880
|
-
return DataMigrator.migrateTable(table, sourceConnection, targetConnection, config);
|
|
881
|
-
});
|
|
882
|
-
for (const [resultIndex, result] of levelResults.entries()) {
|
|
883
|
-
const table = tables[resultIndex];
|
|
884
|
-
progress.processedRows += result.rowsMigrated;
|
|
885
|
-
if (result.errors.length > 0 && table !== undefined) {
|
|
886
|
-
progress.errors[table.name] = result.errors.join('; ');
|
|
887
|
-
}
|
|
888
|
-
}
|
|
1138
|
+
await migrateTableLevel(levelIndex, tables, tableLevels, tableParallelism, sourceConnection, targetConnection, config, progress);
|
|
889
1139
|
}
|
|
890
1140
|
progress.totalRows = Math.max(progress.totalRows, progress.processedRows);
|
|
891
1141
|
// Update final percentage
|
|
@@ -998,8 +1248,15 @@ export const DataMigrator = Object.freeze({
|
|
|
998
1248
|
if (schemaStatements.length === 0) {
|
|
999
1249
|
return;
|
|
1000
1250
|
}
|
|
1001
|
-
|
|
1002
|
-
|
|
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
|
+
}
|
|
1003
1260
|
schemaStatements.length = 0;
|
|
1004
1261
|
};
|
|
1005
1262
|
const pushSchemaStatement = async (sql) => {
|
|
@@ -1050,12 +1307,43 @@ export const DataMigrator = Object.freeze({
|
|
|
1050
1307
|
Logger.info(`Found ${tables.length} tables`);
|
|
1051
1308
|
return { tables };
|
|
1052
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
|
+
},
|
|
1053
1337
|
/**
|
|
1054
1338
|
* Get target table row count for resumability
|
|
1055
1339
|
*/
|
|
1056
1340
|
async getTargetRowCount(targetConnection, tableName) {
|
|
1057
1341
|
if (!targetConnection.adapter)
|
|
1058
1342
|
return 0;
|
|
1343
|
+
if (targetConnection.targetRowCountsCache &&
|
|
1344
|
+
typeof targetConnection.targetRowCountsCache[tableName] === 'number') {
|
|
1345
|
+
return targetConnection.targetRowCountsCache[tableName];
|
|
1346
|
+
}
|
|
1059
1347
|
try {
|
|
1060
1348
|
const result = await targetConnection.adapter.query(`SELECT COUNT(*) as count FROM \`${tableName}\``, []);
|
|
1061
1349
|
const count = result.rows[0]?.['count'];
|
|
@@ -1206,7 +1494,12 @@ export const DataMigrator = Object.freeze({
|
|
|
1206
1494
|
throw ErrorFactory.createValidationError(`No target adapter configured for ${targetConnection.database}`);
|
|
1207
1495
|
}
|
|
1208
1496
|
if (targetConnection.type === 'd1-remote') {
|
|
1209
|
-
|
|
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;
|
|
1210
1503
|
}
|
|
1211
1504
|
const batchSettings = getInsertBatchSettings(targetConnection);
|
|
1212
1505
|
const statements = createInsertStatements(targetConnection.type, batchSettings, tableName, data);
|
|
@@ -1217,6 +1510,10 @@ export const DataMigrator = Object.freeze({
|
|
|
1217
1510
|
insertedRows += affectedRows;
|
|
1218
1511
|
adjustRemoteBatchTuning(targetConnection, affectedRows, executionDurationMs, getSqlByteLength(statement.sql));
|
|
1219
1512
|
}
|
|
1513
|
+
if (targetConnection.targetRowCountsCache &&
|
|
1514
|
+
typeof targetConnection.targetRowCountsCache[tableName] === 'number') {
|
|
1515
|
+
targetConnection.targetRowCountsCache[tableName] += insertedRows;
|
|
1516
|
+
}
|
|
1220
1517
|
return insertedRows;
|
|
1221
1518
|
},
|
|
1222
1519
|
/**
|