@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
|
-
--
|
|
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>[],
|
|
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;
|
|
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
|
@@ -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
|
|
13
|
+
const extractWranglerJson = (output) => {
|
|
14
|
+
const trimmed = output.trim();
|
|
15
|
+
if (!trimmed.startsWith('[')) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
14
18
|
try {
|
|
15
|
-
|
|
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
|
|
22
|
+
return null;
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return
|
|
25
|
+
const normalizeWranglerTableValue = (value) => {
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (trimmed === '') {
|
|
28
|
+
return '';
|
|
29
29
|
}
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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 = `
|
|
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
|
|
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,
|
|
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
|
|
509
|
-
const
|
|
510
|
-
const
|
|
511
|
-
|
|
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
|
|
514
|
-
const values = keys.map((key) => row[key]);
|
|
867
|
+
for (const statement of executableStatements) {
|
|
515
868
|
try {
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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;
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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"
|