@zintrust/d1-migrator 1.9.1 → 1.9.3

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
@@ -143,12 +143,14 @@ zin migrate-to-d1 \
143
143
  zin migrate-to-d1 \
144
144
  --from postgresql \
145
145
  --source-connection "postgresql://user:password@localhost:5432/sourcedb" \
146
- --to d1-remote \
146
+ --remote \
147
147
  --target-database my-d1-remote \
148
148
  --batch-size 5000 \
149
149
  --checkpoint-interval 25000
150
150
  ```
151
151
 
152
+ Use `--remote` to execute against the resolved Wrangler D1 binding with `wrangler d1 ... --remote`. If the target entry in `wrangler.jsonc` has `"remote": true`, `zin migrate-to-d1` also defaults to remote execution automatically for that binding.
153
+
152
154
  #### Dry Run (Test Mode)
153
155
 
154
156
  ```bash
@@ -196,6 +198,7 @@ zin migrate-to-d1 \
196
198
  | ----------------------- | ----- | ------- | -------- | ------- | ------------------------------------------------------------------ |
197
199
  | `--from` | `-f` | string | ✗ | — | Source database type: `mysql`, `postgresql`, `sqlite`, `sqlserver` |
198
200
  | `--to` | `-t` | string | ✗ | `d1` | Target: `d1` (local) or `d1-remote` |
201
+ | `--remote` | — | boolean | ✗ | `false` | Execute target D1 statements through Wrangler remote mode |
199
202
  | `--source-connection` | `-s` | string | ✗ | — | Source connection URI (falls back to env or DB\_\* composition) |
200
203
  | `--target-database` | `-d` | string | ✗ | `d1` | Target D1 database identifier (or env fallback) |
201
204
  | `--batch-size` | `-b` | number | ✗ | `1000` | Records per batch during data copy |
@@ -19,11 +19,18 @@ export interface TargetConnection {
19
19
  database: string;
20
20
  connected: boolean;
21
21
  adapter?: DatabaseAdapter;
22
+ remoteBatchTuning?: RemoteBatchTuning;
22
23
  }
23
24
  export interface TableInfo {
24
25
  name: string;
25
26
  rowCount?: number;
27
+ dependsOn?: string[];
26
28
  }
29
+ type RemoteBatchTuning = {
30
+ rowsPerStatement: number;
31
+ maxStatementSqlLength: number;
32
+ maxExecutionSqlLength: number;
33
+ };
27
34
  type AdapterQueryResult = {
28
35
  rows: Record<string, unknown>[];
29
36
  rowCount?: number;
@@ -77,6 +84,7 @@ export declare const DataMigrator: Readonly<{
77
84
  rowsMigrated: number;
78
85
  errors: string[];
79
86
  }>;
87
+ processTableChunks(table: TableInfo, sourceConnection: SourceConnection, targetConnection: TargetConnection, totalRows: number, batchSize: number, startOffset: number, errors: string[]): Promise<number>;
80
88
  /**
81
89
  * Read data chunk from source database
82
90
  */
@@ -84,7 +92,7 @@ export declare const DataMigrator: Readonly<{
84
92
  /**
85
93
  * Transform data for D1 compatibility
86
94
  */
87
- transformData(chunk: Record<string, unknown>[], tableName: string): Promise<Record<string, unknown>[]>;
95
+ transformData(chunk: Record<string, unknown>[], _tableName: string): Promise<Record<string, unknown>[]>;
88
96
  /**
89
97
  * Insert data into target database
90
98
  */
@@ -1 +1 @@
1
- {"version":3,"file":"DataMigrator.d.ts","sourceRoot":"","sources":["../../src/cli/DataMigrator.ts"],"names":[],"mappings":"AACA;;;GAGG;AAWH,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;CAC3B;AAED,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;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,KAAK,kBAAkB,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACxE,CAAC;AAEF,KAAK,0BAA0B,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAgPF;;;GAGG;AACH,eAAO,MAAM,YAAY;IACvB;;OAEG;wBACuB,eAAe,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAsFtE;;OAEG;4BAC2B,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IA+BzE;;OAEG;4BAC2B,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAyCzE;;OAEG;0CAEiB,gBAAgB,oBAChB,gBAAgB,UAC1B,eAAe,GACtB,OAAO,CAAC,IAAI,CAAC;IAuChB;;OAEG;+BAC8B,gBAAgB,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,SAAS,EAAE,CAAA;KAAE,CAAC;IAmBpF;;OAEG;wCACuC,gBAAgB,aAAa,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAgB/F;;OAEG;wBAEM,SAAS,oBACE,gBAAgB,oBAChB,gBAAgB,UAC1B,eAAe,GACtB,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IA6FtD;;OAEG;oCAEiB,gBAAgB,aACvB,MAAM,UACT,MAAM,aACH,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAkBrC;;OAEG;yBAEM,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,aACrB,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IA4CrC;;OAEG;iCAEiB,gBAAgB,aACvB,MAAM,QACX,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAC9B,OAAO,CAAC,MAAM,CAAC;IAkClB;;OAEG;gCACyB,eAAe,CAAC,cAAc,CAAC,aAAa,MAAM,GAAG,MAAM;IAavF;;OAEG;wCAEM,MAAM,UACL,MAAM,gBACA,MAAM,gBACN,MAAM,GACnB,0BAA0B;IAS7B;;OAEG;gCACyB,MAAM,GAAG,iBAAiB;IAetD;;OAEG;6BAES,iBAAiB,WAClB,OAAO,CAAC,iBAAiB,CAAC,GAClC,iBAAiB;EAGpB,CAAC"}
1
+ {"version":3,"file":"DataMigrator.d.ts","sourceRoot":"","sources":["../../src/cli/DataMigrator.ts"],"names":[],"mappings":"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"}
@@ -3,33 +3,385 @@
3
3
  * Data Migrator
4
4
  * Handles the actual data migration between databases
5
5
  */
6
- import { ErrorFactory, LocalD1Resolver, Logger } from '@zintrust/core';
6
+ import { ErrorFactory, LocalD1Resolver, Logger, WranglerD1 } from '@zintrust/core';
7
7
  import { MySQLAdapter } from '@zintrust/db-mysql';
8
8
  import { PostgreSQLAdapter } from '@zintrust/db-postgres';
9
9
  import { SQLiteAdapter } from '@zintrust/db-sqlite';
10
10
  import { SQLServerAdapter } from '@zintrust/db-sqlserver';
11
11
  import { SchemaBuilder } from '../schema/SchemaBuilder.js';
12
12
  import { SchemaAnalyzer } from './SchemaAnalyzer.js';
13
- const redactConnectionString = (connectionString) => {
13
+ const extractWranglerJson = (output) => {
14
+ const trimmed = output.trim();
15
+ if (!trimmed.startsWith('[')) {
16
+ return null;
17
+ }
14
18
  try {
15
- const parsed = new URL(connectionString);
16
- if (parsed.password.trim() !== '') {
17
- parsed.password = '***';
18
- }
19
- return parsed.toString();
19
+ return JSON.parse(trimmed);
20
20
  }
21
21
  catch {
22
- return connectionString;
22
+ return null;
23
23
  }
24
24
  };
25
- const getUrlPasswordForm = (connectionString) => {
26
- try {
27
- const parsed = new URL(connectionString);
28
- return parsed.password || '';
25
+ const normalizeWranglerTableValue = (value) => {
26
+ const trimmed = value.trim();
27
+ if (trimmed === '') {
28
+ return '';
29
29
  }
30
- catch {
31
- return undefined;
30
+ const normalized = trimmed.toLowerCase();
31
+ if (normalized === 'null') {
32
+ return null;
33
+ }
34
+ if (normalized === 'true') {
35
+ return true;
36
+ }
37
+ if (normalized === 'false') {
38
+ return false;
39
+ }
40
+ if (/^-?\d+$/.test(trimmed)) {
41
+ const parsed = Number.parseInt(trimmed, 10);
42
+ if (Number.isSafeInteger(parsed)) {
43
+ return parsed;
44
+ }
45
+ }
46
+ if (/^-?(?:\d+\.\d+|\d+\.\d*|\.\d+)$/.test(trimmed)) {
47
+ const parsed = Number.parseFloat(trimmed);
48
+ if (Number.isFinite(parsed)) {
49
+ return parsed;
50
+ }
32
51
  }
52
+ return trimmed;
53
+ };
54
+ const parseWranglerTable = (output) => {
55
+ const lines = output.split('\n').map((line) => line.trim());
56
+ const dataLines = lines.filter((line) => line.startsWith('│') && line.endsWith('│'));
57
+ if (dataLines.length < 2) {
58
+ return [];
59
+ }
60
+ const parseCells = (line) => {
61
+ return line
62
+ .slice(1, -1)
63
+ .split('│')
64
+ .map((cell) => cell.trim());
65
+ };
66
+ const headers = parseCells(dataLines[0]);
67
+ const rows = [];
68
+ for (const line of dataLines.slice(1)) {
69
+ const cells = parseCells(line);
70
+ if (cells.length !== headers.length) {
71
+ continue;
72
+ }
73
+ const row = {};
74
+ headers.forEach((header, index) => {
75
+ row[header] = cells[index] ?? '';
76
+ });
77
+ rows.push(row);
78
+ }
79
+ return rows;
80
+ };
81
+ const parseWranglerTableRows = (output) => {
82
+ return parseWranglerTable(output).map((row) => {
83
+ const normalizedRow = {};
84
+ for (const [key, value] of Object.entries(row)) {
85
+ normalizedRow[key] = normalizeWranglerTableValue(value);
86
+ }
87
+ return normalizedRow;
88
+ });
89
+ };
90
+ const toHex = (value) => {
91
+ return Array.from(value)
92
+ .map((byte) => byte.toString(16).padStart(2, '0'))
93
+ .join('');
94
+ };
95
+ const toSqlLiteral = (value) => {
96
+ if (value === null || value === undefined) {
97
+ return 'NULL';
98
+ }
99
+ if (value instanceof Date) {
100
+ return `'${value.toISOString().replace(/'/g, "''")}'`;
101
+ }
102
+ if (typeof value === 'string') {
103
+ return `'${value.replace(/'/g, "''")}'`;
104
+ }
105
+ if (typeof value === 'number') {
106
+ if (!Number.isFinite(value)) {
107
+ throw ErrorFactory.createValidationError('Cannot serialize non-finite number for remote D1');
108
+ }
109
+ return String(value);
110
+ }
111
+ if (typeof value === 'bigint') {
112
+ return value.toString();
113
+ }
114
+ if (typeof value === 'boolean') {
115
+ return value ? '1' : '0';
116
+ }
117
+ const globalBuffer = globalThis;
118
+ if (globalBuffer.Buffer?.isBuffer(value) === true || value instanceof Uint8Array) {
119
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
120
+ return `X'${toHex(bytes)}'`;
121
+ }
122
+ return `'${JSON.stringify(value).replace(/'/g, "''")}'`;
123
+ };
124
+ const bindSqlParameters = (sql, parameters) => {
125
+ let index = 0;
126
+ return sql.replace(/\?/g, () => {
127
+ if (index >= parameters.length) {
128
+ throw ErrorFactory.createValidationError('Remote D1 SQL parameter count mismatch');
129
+ }
130
+ const rendered = toSqlLiteral(parameters[index]);
131
+ index += 1;
132
+ return rendered;
133
+ });
134
+ };
135
+ const REMOTE_INSERT_ROWS_PER_STATEMENT = 200;
136
+ const LOCAL_INSERT_ROWS_PER_STATEMENT = 500;
137
+ const MAX_REMOTE_INSERT_SQL_LENGTH = 100000;
138
+ const MAX_REMOTE_EXECUTION_SQL_LENGTH = 250000;
139
+ const MIN_REMOTE_INSERT_ROWS_PER_STATEMENT = 50;
140
+ const MAX_REMOTE_INSERT_ROWS_PER_STATEMENT = 2000;
141
+ const DEFAULT_REMOTE_TABLE_PARALLELISM = 4;
142
+ const formatDuration = (durationMs) => {
143
+ if (durationMs < 1000) {
144
+ return `${durationMs}ms`;
145
+ }
146
+ return `${(durationMs / 1000).toFixed(durationMs < 10000 ? 2 : 1)}s`;
147
+ };
148
+ const formatRowsPerSecond = (rows, durationMs) => {
149
+ if (rows <= 0 || durationMs <= 0) {
150
+ return 'n/a';
151
+ }
152
+ const rate = rows / (durationMs / 1000);
153
+ return `${rate >= 100 ? rate.toFixed(0) : rate.toFixed(2)} rows/s`;
154
+ };
155
+ const getRemoteBatchTuning = (connection) => {
156
+ if (connection.remoteBatchTuning !== undefined) {
157
+ return connection.remoteBatchTuning;
158
+ }
159
+ connection.remoteBatchTuning = {
160
+ rowsPerStatement: REMOTE_INSERT_ROWS_PER_STATEMENT,
161
+ maxStatementSqlLength: MAX_REMOTE_INSERT_SQL_LENGTH,
162
+ maxExecutionSqlLength: MAX_REMOTE_EXECUTION_SQL_LENGTH,
163
+ };
164
+ return connection.remoteBatchTuning;
165
+ };
166
+ const getInsertBatchSettings = (connection) => {
167
+ if (connection.type === 'd1-remote') {
168
+ const tuning = getRemoteBatchTuning(connection);
169
+ return {
170
+ rowsPerStatement: tuning.rowsPerStatement,
171
+ maxStatementSqlLength: tuning.maxStatementSqlLength,
172
+ maxExecutionSqlLength: tuning.maxExecutionSqlLength,
173
+ };
174
+ }
175
+ return {
176
+ rowsPerStatement: LOCAL_INSERT_ROWS_PER_STATEMENT,
177
+ maxStatementSqlLength: Number.POSITIVE_INFINITY,
178
+ maxExecutionSqlLength: Number.POSITIVE_INFINITY,
179
+ };
180
+ };
181
+ const adjustRemoteBatchTuning = (connection, executedRows, durationMs, sqlLength) => {
182
+ if (connection.type !== 'd1-remote' || executedRows <= 0) {
183
+ return;
184
+ }
185
+ const tuning = getRemoteBatchTuning(connection);
186
+ const previousRowsPerStatement = tuning.rowsPerStatement;
187
+ const nearSqlLimit = sqlLength >= Math.floor(tuning.maxExecutionSqlLength * 0.9);
188
+ if (durationMs <= 1500 &&
189
+ executedRows >= Math.floor(previousRowsPerStatement * 0.8) &&
190
+ !nearSqlLimit) {
191
+ tuning.rowsPerStatement = Math.min(MAX_REMOTE_INSERT_ROWS_PER_STATEMENT, Math.max(previousRowsPerStatement + 25, Math.floor(previousRowsPerStatement * 1.25)));
192
+ }
193
+ else if (durationMs >= 6000 || nearSqlLimit) {
194
+ tuning.rowsPerStatement = Math.max(MIN_REMOTE_INSERT_ROWS_PER_STATEMENT, Math.min(previousRowsPerStatement - 25, Math.floor(previousRowsPerStatement * 0.75)));
195
+ }
196
+ if (tuning.rowsPerStatement !== previousRowsPerStatement) {
197
+ Logger.info(`[DataMigrator] Adaptive remote batching: rows_per_statement ${previousRowsPerStatement} -> ${tuning.rowsPerStatement} after ${executedRows} rows in ${formatDuration(durationMs)}`);
198
+ }
199
+ };
200
+ const getTableDependencies = (table) => {
201
+ if (!Array.isArray(table.dependsOn)) {
202
+ return [];
203
+ }
204
+ return [...new Set(table.dependsOn.filter((dependency) => dependency.trim() !== ''))];
205
+ };
206
+ const buildTableMigrationLevels = (tables) => {
207
+ const tablesByName = new Map();
208
+ for (const table of tables) {
209
+ tablesByName.set(table.name, table);
210
+ }
211
+ const unresolved = new Set(tables.map((table) => table.name));
212
+ const dependenciesByTable = new Map();
213
+ for (const table of tables) {
214
+ dependenciesByTable.set(table.name, new Set(getTableDependencies(table).filter((dependency) => dependency !== table.name && tablesByName.has(dependency))));
215
+ }
216
+ const levels = [];
217
+ while (unresolved.size > 0) {
218
+ const readyNames = tables
219
+ .map((table) => table.name)
220
+ .filter((name) => unresolved.has(name) && (dependenciesByTable.get(name)?.size ?? 0) === 0);
221
+ if (readyNames.length === 0) {
222
+ const cyclicTables = tables.filter((table) => unresolved.has(table.name));
223
+ if (cyclicTables.length > 0) {
224
+ Logger.warn(`[DataMigrator] Table dependency cycle or unresolved reference detected. Falling back to sequential execution for: ${cyclicTables.map((table) => table.name).join(', ')}`);
225
+ levels.push(...cyclicTables.map((table) => [table]));
226
+ }
227
+ break;
228
+ }
229
+ const levelTables = readyNames
230
+ .map((name) => tablesByName.get(name))
231
+ .filter((table) => table !== undefined);
232
+ levels.push(levelTables);
233
+ for (const readyName of readyNames) {
234
+ unresolved.delete(readyName);
235
+ }
236
+ for (const name of unresolved) {
237
+ const dependencies = dependenciesByTable.get(name);
238
+ for (const readyName of readyNames) {
239
+ dependencies?.delete(readyName);
240
+ }
241
+ }
242
+ }
243
+ return levels;
244
+ };
245
+ const getTableParallelism = (config, targetConnection) => {
246
+ if (targetConnection.type !== 'd1-remote') {
247
+ return 1;
248
+ }
249
+ if (config.sourceDriver === 'sqlite') {
250
+ return 1;
251
+ }
252
+ return DEFAULT_REMOTE_TABLE_PARALLELISM;
253
+ };
254
+ const executeWithConcurrency = async (items, concurrency, worker) => {
255
+ if (items.length === 0) {
256
+ return [];
257
+ }
258
+ const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
259
+ const results = new Array(items.length);
260
+ let index = 0;
261
+ const runWorker = async () => {
262
+ while (index < items.length) {
263
+ const currentIndex = index;
264
+ index += 1;
265
+ results[currentIndex] = await worker(items[currentIndex]);
266
+ }
267
+ };
268
+ await Promise.all(Array.from({ length: effectiveConcurrency }, () => runWorker()));
269
+ return results;
270
+ };
271
+ const estimateRemoteRowSqlLength = (keys, row) => {
272
+ const delimitersLength = keys.length > 0 ? (keys.length - 1) * 2 : 0;
273
+ const valuesLength = keys.reduce((total, key) => {
274
+ return total + toSqlLiteral(row[key]).length;
275
+ }, 0);
276
+ return valuesLength + delimitersLength + 2;
277
+ };
278
+ const createInsertStatements = (targetType, settings, tableName, data) => {
279
+ if (data.length === 0) {
280
+ return [];
281
+ }
282
+ const keys = Object.keys(data[0]);
283
+ const columnList = keys.map((key) => `\`${key}\``).join(', ');
284
+ const rowPlaceholder = `(${keys.map(() => '?').join(', ')})`;
285
+ const prefix = `INSERT INTO \`${tableName}\` (${columnList}) VALUES `;
286
+ const rowLimit = settings.rowsPerStatement;
287
+ const maxSqlLength = settings.maxStatementSqlLength;
288
+ const statements = [];
289
+ let batchRows = [];
290
+ let batchParameters = [];
291
+ let batchSqlLength = prefix.length;
292
+ const flushBatch = () => {
293
+ if (batchRows.length === 0) {
294
+ return;
295
+ }
296
+ statements.push({
297
+ sql: `${prefix}${batchRows.map(() => rowPlaceholder).join(', ')}`,
298
+ parameters: batchParameters,
299
+ rowCount: batchRows.length,
300
+ });
301
+ batchRows = [];
302
+ batchParameters = [];
303
+ batchSqlLength = prefix.length;
304
+ };
305
+ for (const row of data) {
306
+ const rowParameters = keys.map((key) => row[key]);
307
+ const rowSqlLength = targetType === 'd1-remote' ? estimateRemoteRowSqlLength(keys, row) : rowPlaceholder.length;
308
+ const separatorLength = batchRows.length > 0 ? 2 : 0;
309
+ const nextSqlLength = batchSqlLength + separatorLength + rowSqlLength;
310
+ if (batchRows.length > 0 && (batchRows.length >= rowLimit || nextSqlLength > maxSqlLength)) {
311
+ flushBatch();
312
+ }
313
+ batchRows.push(row);
314
+ batchParameters.push(...rowParameters);
315
+ batchSqlLength += (batchRows.length > 1 ? 2 : 0) + rowSqlLength;
316
+ }
317
+ flushBatch();
318
+ return statements;
319
+ };
320
+ const createRemoteExecutionBatchesWithLimit = (statements, maxExecutionSqlLength) => {
321
+ if (statements.length <= 1) {
322
+ return statements;
323
+ }
324
+ const batches = [];
325
+ let sqlParts = [];
326
+ let parameters = [];
327
+ let rowCount = 0;
328
+ let currentLength = 0;
329
+ const flushBatch = () => {
330
+ if (sqlParts.length === 0) {
331
+ return;
332
+ }
333
+ batches.push({
334
+ sql: sqlParts.join(';\n'),
335
+ parameters,
336
+ rowCount,
337
+ });
338
+ sqlParts = [];
339
+ parameters = [];
340
+ rowCount = 0;
341
+ currentLength = 0;
342
+ };
343
+ for (const statement of statements) {
344
+ const separatorLength = sqlParts.length > 0 ? 2 : 0;
345
+ const nextLength = currentLength + separatorLength + statement.sql.length;
346
+ if (sqlParts.length > 0 && nextLength > maxExecutionSqlLength) {
347
+ flushBatch();
348
+ }
349
+ sqlParts.push(statement.sql);
350
+ parameters.push(...statement.parameters);
351
+ rowCount += statement.rowCount;
352
+ currentLength += (sqlParts.length > 1 ? 2 : 0) + statement.sql.length;
353
+ }
354
+ flushBatch();
355
+ return batches;
356
+ };
357
+ const createRemoteD1Adapter = (database) => {
358
+ return {
359
+ async connect() {
360
+ await Promise.resolve();
361
+ },
362
+ async disconnect() {
363
+ await Promise.resolve();
364
+ },
365
+ async query(sql, parameters) {
366
+ const renderedSql = bindSqlParameters(sql, parameters);
367
+ const output = WranglerD1.executeSql({ dbName: database, isLocal: false, sql: renderedSql });
368
+ const payload = extractWranglerJson(output);
369
+ if (payload === null || payload.length === 0) {
370
+ const rows = parseWranglerTableRows(output);
371
+ return { rows, rowCount: rows.length };
372
+ }
373
+ const last = payload.at(-1);
374
+ if (last === undefined) {
375
+ return { rows: [], rowCount: 0 };
376
+ }
377
+ const rows = Array.isArray(last.results) ? last.results : [];
378
+ const totalChanges = payload.reduce((count, statement) => {
379
+ return count + (typeof statement.meta?.changes === 'number' ? statement.meta.changes : 0);
380
+ }, 0);
381
+ const rowCount = totalChanges > 0 ? totalChanges : rows.length;
382
+ return { rows, rowCount };
383
+ },
384
+ };
33
385
  };
34
386
  const getErrorCause = (error) => {
35
387
  if (error === null || typeof error !== 'object') {
@@ -82,11 +434,6 @@ const logDetailedError = (label, error) => {
82
434
  logDetailedError(`${label} cause`, cause);
83
435
  }
84
436
  };
85
- const describePasswordForLog = (password) => {
86
- const hasSpecialCharacters = /[^a-zA-Z0-9]/.test(password);
87
- const containsBang = password.includes('!');
88
- return `len=${password.length}, special_chars=${hasSpecialCharacters}, contains_bang=${containsBang}`;
89
- };
90
437
  const normalizeNullLikeValue = (value) => {
91
438
  if (typeof value !== 'string')
92
439
  return value;
@@ -132,11 +479,6 @@ const createSourceAdapter = (config) => {
132
479
  switch (config.sourceDriver) {
133
480
  case 'mysql': {
134
481
  const connectionDetails = parseConnectionDetails(config.sourceConnection, 3306, 'mysql', 'root');
135
- const urlPasswordForm = getUrlPasswordForm(config.sourceConnection);
136
- Logger.info(`[DataMigrator] Source password diagnostics: ${describePasswordForLog(connectionDetails.password)}`);
137
- if (urlPasswordForm !== undefined) {
138
- Logger.info(`[DataMigrator] MySQL password handoff: url_form(${describePasswordForLog(urlPasswordForm)}), final_auth(${describePasswordForLog(connectionDetails.password)}), matches=${urlPasswordForm === connectionDetails.password}`);
139
- }
140
482
  return MySQLAdapter.create({
141
483
  driver: 'mysql',
142
484
  host: connectionDetails.host,
@@ -227,14 +569,20 @@ export const DataMigrator = Object.freeze({
227
569
  if (targetConnection.adapter) {
228
570
  await DataMigrator.prepareTargetSchema(sourceConnection, targetConnection, config);
229
571
  }
230
- // Migrate each table sequentially for reliable D1/SQLite writes
231
572
  Logger.info('Starting table migration...');
232
- for (const table of schema.tables) {
233
- const result = await DataMigrator.migrateTable(table, sourceConnection, targetConnection, config);
234
- progress.processedRows += result.rowsMigrated;
235
- // Add any errors to progress
236
- if (result.errors.length > 0) {
237
- progress.errors[table.name] = result.errors.join('; ');
573
+ const tableLevels = buildTableMigrationLevels(schema.tables);
574
+ const tableParallelism = getTableParallelism(config, targetConnection);
575
+ for (const [levelIndex, tables] of tableLevels.entries()) {
576
+ Logger.info(`[DataMigrator] Starting table level ${levelIndex + 1}/${tableLevels.length}: ${tables.map((table) => table.name).join(', ')}`);
577
+ const levelResults = await executeWithConcurrency(tables, tableParallelism, async (table) => {
578
+ return DataMigrator.migrateTable(table, sourceConnection, targetConnection, config);
579
+ });
580
+ for (const [resultIndex, result] of levelResults.entries()) {
581
+ const table = tables[resultIndex];
582
+ progress.processedRows += result.rowsMigrated;
583
+ if (result.errors.length > 0 && table !== undefined) {
584
+ progress.errors[table.name] = result.errors.join('; ');
585
+ }
238
586
  }
239
587
  }
240
588
  progress.totalRows = Math.max(progress.totalRows, progress.processedRows);
@@ -264,7 +612,6 @@ export const DataMigrator = Object.freeze({
264
612
  */
265
613
  async connectToSource(config) {
266
614
  Logger.info(`Connecting to ${config.sourceDriver} database...`);
267
- Logger.info(`[DataMigrator] Source connection (redacted): ${redactConnectionString(config.sourceConnection)}`);
268
615
  const adapter = createSourceAdapter(config);
269
616
  try {
270
617
  await adapter.connect();
@@ -315,6 +662,11 @@ export const DataMigrator = Object.freeze({
315
662
  throw ErrorFactory.createConnectionError(`Unable to connect resolved local D1 path ${d1LocalPath}: ${String(error)}`);
316
663
  }
317
664
  }
665
+ else {
666
+ Logger.info(`[DataMigrator] Using Wrangler remote D1 target: ${config.targetDatabase}`);
667
+ connection.adapter = createRemoteD1Adapter(config.targetDatabase);
668
+ await connection.adapter.connect();
669
+ }
318
670
  Logger.info('✓ Target D1 database connected');
319
671
  return connection;
320
672
  },
@@ -360,6 +712,7 @@ export const DataMigrator = Object.freeze({
360
712
  const tables = sourceSchema.tables.map((table) => ({
361
713
  name: table.name,
362
714
  rowCount: table.rowCount || 0,
715
+ dependsOn: table.foreignKeys.map((foreignKey) => foreignKey.referencedTable),
363
716
  }));
364
717
  Logger.info(`Found ${tables.length} tables`);
365
718
  return { tables };
@@ -387,10 +740,10 @@ export const DataMigrator = Object.freeze({
387
740
  Logger.info(`Migrating table: ${table.name}`);
388
741
  const errors = [];
389
742
  let rowsMigrated = 0;
743
+ const tableStartTime = Date.now();
390
744
  try {
391
745
  const totalRows = table.rowCount || 0;
392
746
  const batchSize = config.batchSize || 1000;
393
- // Check if table is already synced for resumability
394
747
  const targetRowCount = await DataMigrator.getTargetRowCount(targetConnection, table.name);
395
748
  if (targetRowCount >= totalRows) {
396
749
  Logger.info(`Table ${table.name} already synced: ${targetRowCount}/${totalRows} rows, skipping`);
@@ -402,52 +755,54 @@ export const DataMigrator = Object.freeze({
402
755
  else {
403
756
  Logger.info(`Processing ${totalRows} rows in batches of ${batchSize}`);
404
757
  }
405
- // Process data in chunks sequentially for data integrity
406
- // Start from the last synced offset for resumability
407
- const startOffset = targetRowCount;
408
- for (let offset = startOffset; offset < totalRows; offset += batchSize) {
409
- try {
410
- const chunk = await DataMigrator.readDataChunk(sourceConnection, table.name, offset, batchSize);
411
- if (chunk.length === 0)
412
- break;
413
- // Transform data for D1 compatibility
414
- const transformedChunk = await DataMigrator.transformData(chunk, table.name);
415
- // Insert data into target
416
- const insertedRows = await DataMigrator.insertData(targetConnection, table.name, transformedChunk);
417
- if (insertedRows !== chunk.length) {
418
- const verificationError = DataMigrator.createChunkVerificationError(table.name, offset, chunk.length, insertedRows);
419
- throw ErrorFactory.createValidationError(`Chunk insert mismatch on ${table.name}`, verificationError);
420
- }
421
- rowsMigrated += insertedRows;
422
- // Log progress for large tables
423
- if (totalRows > 10000 && rowsMigrated % (batchSize * 10) === 0) {
424
- const normalizedTotalRows = Math.max(totalRows, rowsMigrated);
425
- const percentage = Math.round((rowsMigrated / normalizedTotalRows) * 100);
426
- Logger.info(`Table ${table.name}: ${rowsMigrated}/${normalizedTotalRows} (${percentage}%)`);
427
- }
428
- }
429
- catch (error) {
430
- const errorMsg = `Chunk processing failed at offset ${offset}: ${error}`;
431
- Logger.error(errorMsg);
432
- errors.push(errorMsg);
433
- // Continue with next chunk instead of failing completely
434
- continue;
435
- }
436
- }
437
- Logger.info(`Table ${table.name} completed: ${rowsMigrated} rows migrated`);
758
+ rowsMigrated = await DataMigrator.processTableChunks(table, sourceConnection, targetConnection, totalRows, batchSize, targetRowCount, errors);
759
+ const tableDurationMs = Date.now() - tableStartTime;
760
+ Logger.info(`[DataMigrator] Table ${table.name} completed rows=${rowsMigrated}/${totalRows} duration=${formatDuration(tableDurationMs)} rate=${formatRowsPerSecond(rowsMigrated, tableDurationMs)}`);
761
+ return { rowsMigrated, errors };
438
762
  }
439
763
  catch (error) {
440
- const errorMsg = `Failed to migrate table ${table.name}: ${error}`;
764
+ const errorMsg = `Table migration failed for ${table.name}: ${error}`;
441
765
  Logger.error(errorMsg);
442
766
  errors.push(errorMsg);
767
+ return { rowsMigrated, errors };
768
+ }
769
+ },
770
+ async processTableChunks(table, sourceConnection, targetConnection, totalRows, batchSize, startOffset, errors) {
771
+ let rowsMigrated = 0;
772
+ for (let offset = startOffset; offset < totalRows; offset += batchSize) {
773
+ try {
774
+ const chunkStartTime = Date.now();
775
+ const chunk = await DataMigrator.readDataChunk(sourceConnection, table.name, offset, batchSize);
776
+ if (chunk.length === 0) {
777
+ break;
778
+ }
779
+ const transformedChunk = await DataMigrator.transformData(chunk, table.name);
780
+ const insertedRows = await DataMigrator.insertData(targetConnection, table.name, transformedChunk);
781
+ if (insertedRows !== chunk.length) {
782
+ const verificationError = DataMigrator.createChunkVerificationError(table.name, offset, chunk.length, insertedRows);
783
+ throw ErrorFactory.createValidationError(`Chunk insert mismatch on ${table.name}`, verificationError);
784
+ }
785
+ rowsMigrated += insertedRows;
786
+ const chunkDurationMs = Date.now() - chunkStartTime;
787
+ Logger.info(`[DataMigrator] Chunk ${table.name} offset=${offset} rows=${insertedRows} duration=${formatDuration(chunkDurationMs)} rate=${formatRowsPerSecond(insertedRows, chunkDurationMs)}`);
788
+ if (totalRows > 10000 && rowsMigrated % (batchSize * 10) === 0) {
789
+ const normalizedTotalRows = Math.max(totalRows, rowsMigrated);
790
+ const percentage = Math.round((rowsMigrated / normalizedTotalRows) * 100);
791
+ Logger.info(`Table ${table.name}: ${rowsMigrated}/${normalizedTotalRows} (${percentage}%)`);
792
+ }
793
+ }
794
+ catch (error) {
795
+ const errorMsg = `Chunk processing failed at offset ${offset}: ${error}`;
796
+ Logger.error(errorMsg);
797
+ errors.push(errorMsg);
798
+ }
443
799
  }
444
- return { rowsMigrated, errors };
800
+ return rowsMigrated;
445
801
  },
446
802
  /**
447
803
  * Read data chunk from source database
448
804
  */
449
805
  async readDataChunk(sourceConnection, tableName, offset, batchSize) {
450
- Logger.debug(`Reading chunk from ${tableName}: offset ${offset}, size ${batchSize}`);
451
806
  if (!sourceConnection.adapter)
452
807
  return [];
453
808
  try {
@@ -463,8 +818,7 @@ export const DataMigrator = Object.freeze({
463
818
  /**
464
819
  * Transform data for D1 compatibility
465
820
  */
466
- async transformData(chunk, tableName) {
467
- Logger.debug(`Transforming ${chunk.length} rows for table ${tableName}`);
821
+ async transformData(chunk, _tableName) {
468
822
  return chunk.map((row) => {
469
823
  const transformed = {};
470
824
  for (const [key, rawValue] of Object.entries(row)) {
@@ -499,27 +853,30 @@ export const DataMigrator = Object.freeze({
499
853
  * Insert data into target database
500
854
  */
501
855
  async insertData(targetConnection, tableName, data) {
502
- Logger.debug(`Inserting ${data.length} rows into ${tableName}`);
503
856
  if (data.length === 0)
504
857
  return 0;
505
858
  if (!targetConnection.adapter) {
506
859
  throw ErrorFactory.createValidationError(`No target adapter configured for ${targetConnection.database}`);
507
860
  }
508
- const keys = Object.keys(data[0]);
509
- const columnList = keys.map((key) => `\`${key}\``).join(', ');
510
- const placeholders = keys.map(() => '?').join(', ');
511
- const sql = `INSERT INTO \`${tableName}\` (${columnList}) VALUES (${placeholders})`;
861
+ const batchSettings = getInsertBatchSettings(targetConnection);
862
+ const statements = createInsertStatements(targetConnection.type, batchSettings, tableName, data);
863
+ const executableStatements = targetConnection.type === 'd1-remote'
864
+ ? createRemoteExecutionBatchesWithLimit(statements, batchSettings.maxExecutionSqlLength)
865
+ : statements;
512
866
  let insertedRows = 0;
513
- for (const row of data) {
514
- const values = keys.map((key) => row[key]);
867
+ for (const statement of executableStatements) {
515
868
  try {
516
- await targetConnection.adapter.query(sql, values);
517
- insertedRows += 1;
869
+ const executionStartTime = Date.now();
870
+ const result = await targetConnection.adapter.query(statement.sql, statement.parameters);
871
+ const executionDurationMs = Date.now() - executionStartTime;
872
+ const affectedRows = typeof result.rowCount === 'number' ? result.rowCount : statement.rowCount;
873
+ insertedRows += affectedRows;
874
+ adjustRemoteBatchTuning(targetConnection, affectedRows, executionDurationMs, statement.sql.length);
518
875
  }
519
876
  catch (error) {
520
877
  throw ErrorFactory.createValidationError(`Insert failed for table ${tableName}`, {
521
- sql,
522
- row,
878
+ sql: statement.sql,
879
+ rowCount: statement.rowCount,
523
880
  cause: error,
524
881
  });
525
882
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MigrateToD1Command.d.ts","sourceRoot":"","sources":["../../src/cli/MigrateToD1Command.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAe,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,UAAU,CAAC;AAOxE,KAAK,iBAAiB,GAAG;IACvB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,IAAI,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B,CAAC;AAgxBF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,iBAgH/B,CAAC;AAEH;;GAEG;AACH,iBAAe,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAetE;AAED;;GAEG;AACH,iBAAe,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAmGxE;AAED;;GAEG;AACH,iBAAe,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAkGtE;AAED;;GAEG;AACH,iBAAS,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA2BrF;AAGD,eAAO,MAAM,iBAAiB;;;;;EAK5B,CAAC"}
1
+ {"version":3,"file":"MigrateToD1Command.d.ts","sourceRoot":"","sources":["../../src/cli/MigrateToD1Command.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAe,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,OAAO,KAAK,EAAE,eAAe,EAA0B,MAAM,UAAU,CAAC;AAOxE,KAAK,iBAAiB,GAAG;IACvB,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,IAAI,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,CAAC;IACxC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvD,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;CAC/B,CAAC;AAouBF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,iBA0G/B,CAAC;AAEH;;GAEG;AACH,iBAAe,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAetE;AAED;;GAEG;AACH,iBAAe,kBAAkB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAmGxE;AAED;;GAEG;AACH,iBAAe,gBAAgB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAkGtE;AAED;;GAEG;AACH,iBAAS,cAAc,CAAC,MAAM,EAAE,eAAe,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CA2BrF;AAGD,eAAO,MAAM,iBAAiB;;;;;EAK5B,CAAC"}
@@ -229,35 +229,6 @@ const normalizeSourceConnectionString = (sourceConnection, sourceDriver, origin)
229
229
  }
230
230
  return buildNetworkConnectionString(details);
231
231
  };
232
- const redactConnectionString = (sourceConnection) => {
233
- try {
234
- const parsed = new URL(sourceConnection);
235
- if (parsed.password.trim() !== '') {
236
- parsed.password = '***';
237
- }
238
- return parsed.toString();
239
- }
240
- catch {
241
- return sourceConnection;
242
- }
243
- };
244
- const getPasswordForDiagnostics = (sourceConnection, sourceDriver, origin) => {
245
- if (origin === 'db-env') {
246
- return parseNetworkConnectionDetails(sourceConnection, sourceDriver)?.password;
247
- }
248
- try {
249
- const parsed = new URL(sourceConnection);
250
- return parsed.password;
251
- }
252
- catch {
253
- return undefined;
254
- }
255
- };
256
- const describePasswordForLog = (password) => {
257
- const hasSpecialCharacters = /[^a-zA-Z0-9]/.test(password);
258
- const containsBang = password.includes('!');
259
- return `len=${password.length}, special_chars=${hasSpecialCharacters}, contains_bang=${containsBang}`;
260
- };
261
232
  const getErrorCause = (error) => {
262
233
  if (error === null || typeof error !== 'object') {
263
234
  return undefined;
@@ -309,18 +280,6 @@ const logDetailedError = (label, error) => {
309
280
  logDetailedError(`${label} cause`, cause);
310
281
  }
311
282
  };
312
- const logSourceConnectionDiagnostics = (sourceDriver, sourceConnection, origin, originalValue) => {
313
- Logger.info(`[d1-migrator] Source connection origin: ${origin}`);
314
- Logger.info(`[d1-migrator] Source connection driver: ${sourceDriver}`);
315
- Logger.info(`[d1-migrator] Source connection (redacted): ${redactConnectionString(sourceConnection)}`);
316
- const originalPassword = getPasswordForDiagnostics(originalValue, sourceDriver, origin);
317
- const finalPassword = getPasswordForDiagnostics(sourceConnection, sourceDriver, origin);
318
- if (originalPassword === undefined || finalPassword === undefined) {
319
- Logger.info('[d1-migrator] Source connection diagnostics: non-network source or unable to parse URL');
320
- return;
321
- }
322
- Logger.info(`[d1-migrator] Source password diagnostics: provided(${describePasswordForLog(originalPassword)}), final(${describePasswordForLog(finalPassword)}), matches=${originalPassword === finalPassword}`);
323
- };
324
283
  const normalizeSourceDriver = (value) => {
325
284
  if (value === undefined) {
326
285
  return undefined;
@@ -461,6 +420,9 @@ const resolveSourceConnection = (options, sourceDriver) => {
461
420
  throw ErrorFactory.createValidationError('Source connection is required. Use --source-connection or set MIGRATE_TO_D1_SOURCE_CONNECTION (or DB_* variables)');
462
421
  };
463
422
  const resolveTargetType = (options) => {
423
+ if (readOptionFlag(options, ['remote'])) {
424
+ return 'd1-remote';
425
+ }
464
426
  const fromOption = readOptionString(options, ['to']);
465
427
  const fromEnv = readEnvString(TARGET_TYPE_ENV_KEYS);
466
428
  const configuredValue = fromOption ?? fromEnv;
@@ -468,7 +430,19 @@ const resolveTargetType = (options) => {
468
430
  if (configuredValue !== undefined && targetType === undefined) {
469
431
  throw ErrorFactory.createValidationError(`Unsupported target type: ${configuredValue}. Expected d1 or d1-remote`);
470
432
  }
471
- return targetType ?? 'd1';
433
+ if (targetType !== undefined) {
434
+ return targetType;
435
+ }
436
+ try {
437
+ const resolvedTarget = WranglerConfig.getD1Database(process.cwd(), resolveTargetDatabase(options));
438
+ if (resolvedTarget?.remote === true) {
439
+ return 'd1-remote';
440
+ }
441
+ }
442
+ catch {
443
+ // Fall back to local default when target resolution is unavailable here.
444
+ }
445
+ return 'd1';
472
446
  };
473
447
  const resolveSourceSsl = (options) => {
474
448
  if (readOptionFlag(options, ['source-ssl', 'sourceSsl'])) {
@@ -548,6 +522,7 @@ export const MigrateToD1Command = BaseCommand.create({
548
522
  command
549
523
  .option('-f, --from <type>', 'Source database type (mysql, postgresql, sqlite, sqlserver)')
550
524
  .option('-t, --to <type>', 'Target D1 type (d1, d1-remote)')
525
+ .option('--remote', 'Use Wrangler remote D1 execution for the resolved target binding')
551
526
  .option('-s, --source-connection <string>', 'Source database connection string')
552
527
  .option('--source-ssl', 'Force SSL/TLS for source database connection')
553
528
  .option('-d, --target-database <string>', 'Target D1 database name')
@@ -562,8 +537,7 @@ export const MigrateToD1Command = BaseCommand.create({
562
537
  execute: async (options) => {
563
538
  try {
564
539
  Logger.info('Starting D1 migration process...');
565
- const { config, schemaOnly, sourceConnectionOrigin, originalSourceConnection } = resolveMigrationConfig(options);
566
- logSourceConnectionDiagnostics(config.sourceDriver, config.sourceConnection, sourceConnectionOrigin, originalSourceConnection);
540
+ const { config, schemaOnly } = resolveMigrationConfig(options);
567
541
  const configValidation = validateConfig(config);
568
542
  if (!configValidation.valid) {
569
543
  throw ErrorFactory.createValidationError(`Invalid migration configuration: ${configValidation.errors.join(', ')}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zintrust/d1-migrator",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
4
4
  "description": "Resumable database migration toolkit for moving data to Cloudflare D1 with ZinTrust.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@zintrust/db-d1": "^1.8.0",
44
- "@zintrust/db-mysql": "^1.8.9",
44
+ "@zintrust/db-mysql": "^1.9.0",
45
45
  "@zintrust/db-postgres": "^1.8.0",
46
46
  "@zintrust/db-sqlite": "^1.8.0",
47
47
  "@zintrust/db-sqlserver": "^1.8.0"