@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 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';
@@ -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 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) {
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
- 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;
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
- Logger.info(`[DataMigrator] Starting table level ${levelIndex + 1}/${tableLevels.length}: ${tables.map((table) => table.name).join(', ')}`);
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
- const batchSql = `${schemaStatements.join(';\n')};`;
1002
- 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
+ }
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
- 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;
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/d1-migrator",
3
- "version": "2.0.3",
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",