@zintrust/d1-migrator 1.9.2 → 1.9.5
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/dist/cli/DataMigrator.d.ts +19 -1
- package/dist/cli/DataMigrator.d.ts.map +1 -1
- package/dist/cli/DataMigrator.js +617 -107
- package/dist/cli/MigrateToD1Command.d.ts.map +1 -1
- package/dist/cli/MigrateToD1Command.js +1 -43
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -13,17 +13,34 @@ export interface SourceConnection {
|
|
|
13
13
|
sourceSsl?: boolean;
|
|
14
14
|
connected: boolean;
|
|
15
15
|
adapter?: DatabaseAdapter;
|
|
16
|
+
sourceBatchTuning?: SourceBatchTuning;
|
|
16
17
|
}
|
|
18
|
+
type SourceBatchHistoryEntry = {
|
|
19
|
+
rowsRead: number;
|
|
20
|
+
durationMs: number;
|
|
21
|
+
timePerRow: number;
|
|
22
|
+
};
|
|
23
|
+
type SourceBatchTuning = {
|
|
24
|
+
readBatchSize: number;
|
|
25
|
+
readBatchHistory: SourceBatchHistoryEntry[];
|
|
26
|
+
};
|
|
17
27
|
export interface TargetConnection {
|
|
18
28
|
type: 'd1' | 'd1-remote';
|
|
19
29
|
database: string;
|
|
20
30
|
connected: boolean;
|
|
21
31
|
adapter?: DatabaseAdapter;
|
|
32
|
+
remoteBatchTuning?: RemoteBatchTuning;
|
|
22
33
|
}
|
|
23
34
|
export interface TableInfo {
|
|
24
35
|
name: string;
|
|
25
36
|
rowCount?: number;
|
|
37
|
+
dependsOn?: string[];
|
|
26
38
|
}
|
|
39
|
+
type RemoteBatchTuning = {
|
|
40
|
+
rowsPerStatement: number;
|
|
41
|
+
maxStatementSqlLength: number;
|
|
42
|
+
maxExecutionSqlLength: number;
|
|
43
|
+
};
|
|
27
44
|
type AdapterQueryResult = {
|
|
28
45
|
rows: Record<string, unknown>[];
|
|
29
46
|
rowCount?: number;
|
|
@@ -77,6 +94,7 @@ export declare const DataMigrator: Readonly<{
|
|
|
77
94
|
rowsMigrated: number;
|
|
78
95
|
errors: string[];
|
|
79
96
|
}>;
|
|
97
|
+
processTableChunks(table: TableInfo, sourceConnection: SourceConnection, targetConnection: TargetConnection, totalRows: number, batchSize: number, startOffset: number, errors: string[]): Promise<number>;
|
|
80
98
|
/**
|
|
81
99
|
* Read data chunk from source database
|
|
82
100
|
*/
|
|
@@ -84,7 +102,7 @@ export declare const DataMigrator: Readonly<{
|
|
|
84
102
|
/**
|
|
85
103
|
* Transform data for D1 compatibility
|
|
86
104
|
*/
|
|
87
|
-
transformData(chunk: Record<string, unknown>[],
|
|
105
|
+
transformData(chunk: Record<string, unknown>[], _tableName: string): Promise<Record<string, unknown>[]>;
|
|
88
106
|
/**
|
|
89
107
|
* Insert data into target database
|
|
90
108
|
*/
|
|
@@ -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;
|
|
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
|
@@ -22,6 +22,35 @@ const extractWranglerJson = (output) => {
|
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
|
+
const normalizeWranglerTableValue = (value) => {
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (trimmed === '') {
|
|
28
|
+
return '';
|
|
29
|
+
}
|
|
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
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return trimmed;
|
|
53
|
+
};
|
|
25
54
|
const parseWranglerTable = (output) => {
|
|
26
55
|
const lines = output.split('\n').map((line) => line.trim());
|
|
27
56
|
const dataLines = lines.filter((line) => line.startsWith('│') && line.endsWith('│'));
|
|
@@ -49,6 +78,15 @@ const parseWranglerTable = (output) => {
|
|
|
49
78
|
}
|
|
50
79
|
return rows;
|
|
51
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
|
+
};
|
|
52
90
|
const toHex = (value) => {
|
|
53
91
|
return Array.from(value)
|
|
54
92
|
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
@@ -59,10 +97,10 @@ const toSqlLiteral = (value) => {
|
|
|
59
97
|
return 'NULL';
|
|
60
98
|
}
|
|
61
99
|
if (value instanceof Date) {
|
|
62
|
-
return `'${value.toISOString().
|
|
100
|
+
return `'${value.toISOString().replaceAll("'", "''")}'`;
|
|
63
101
|
}
|
|
64
102
|
if (typeof value === 'string') {
|
|
65
|
-
return `'${value.
|
|
103
|
+
return `'${value.replaceAll("'", "''")}'`;
|
|
66
104
|
}
|
|
67
105
|
if (typeof value === 'number') {
|
|
68
106
|
if (!Number.isFinite(value)) {
|
|
@@ -81,11 +119,11 @@ const toSqlLiteral = (value) => {
|
|
|
81
119
|
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
82
120
|
return `X'${toHex(bytes)}'`;
|
|
83
121
|
}
|
|
84
|
-
return `'${JSON.stringify(value).
|
|
122
|
+
return `'${JSON.stringify(value).replaceAll("'", "''")}'`;
|
|
85
123
|
};
|
|
86
124
|
const bindSqlParameters = (sql, parameters) => {
|
|
87
125
|
let index = 0;
|
|
88
|
-
return sql.
|
|
126
|
+
return sql.replaceAll('?', () => {
|
|
89
127
|
if (index >= parameters.length) {
|
|
90
128
|
throw ErrorFactory.createValidationError('Remote D1 SQL parameter count mismatch');
|
|
91
129
|
}
|
|
@@ -94,6 +132,460 @@ const bindSqlParameters = (sql, parameters) => {
|
|
|
94
132
|
return rendered;
|
|
95
133
|
});
|
|
96
134
|
};
|
|
135
|
+
const REMOTE_INSERT_ROWS_PER_STATEMENT = 1000;
|
|
136
|
+
const LOCAL_INSERT_ROWS_PER_STATEMENT = 500;
|
|
137
|
+
const MAX_REMOTE_INSERT_SQL_LENGTH = 300000;
|
|
138
|
+
const MAX_REMOTE_EXECUTION_SQL_LENGTH = 800000;
|
|
139
|
+
const MIN_REMOTE_INSERT_ROWS_PER_STATEMENT = 100;
|
|
140
|
+
const MAX_REMOTE_INSERT_ROWS_PER_STATEMENT = 3000;
|
|
141
|
+
const DEFAULT_REMOTE_TABLE_PARALLELISM = 8;
|
|
142
|
+
const DEFAULT_SOURCE_READ_BATCH_SIZE = 250;
|
|
143
|
+
const MIN_SOURCE_READ_BATCH_SIZE = 50;
|
|
144
|
+
const MAX_SOURCE_READ_BATCH_SIZE = 1500;
|
|
145
|
+
const SOURCE_READ_TARGET_TIME_MS = 5000;
|
|
146
|
+
const SOURCE_CONNECT_RETRY_ATTEMPTS = 3;
|
|
147
|
+
const SOURCE_CONNECT_RETRY_BASE_DELAY_MS = 500;
|
|
148
|
+
const formatDuration = (durationMs) => {
|
|
149
|
+
if (durationMs < 1000) {
|
|
150
|
+
return `${durationMs}ms`;
|
|
151
|
+
}
|
|
152
|
+
return `${(durationMs / 1000).toFixed(durationMs < 10000 ? 2 : 1)}s`;
|
|
153
|
+
};
|
|
154
|
+
const formatRowsPerSecond = (rows, durationMs) => {
|
|
155
|
+
if (rows <= 0 || durationMs <= 0) {
|
|
156
|
+
return 'n/a';
|
|
157
|
+
}
|
|
158
|
+
const rate = rows / (durationMs / 1000);
|
|
159
|
+
return `${rate >= 100 ? rate.toFixed(0) : rate.toFixed(2)} rows/s`;
|
|
160
|
+
};
|
|
161
|
+
const waitMs = async (durationMs) => {
|
|
162
|
+
await new Promise((resolve) => {
|
|
163
|
+
const start = Date.now();
|
|
164
|
+
const poll = () => {
|
|
165
|
+
if (Date.now() - start >= durationMs) {
|
|
166
|
+
resolve();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
globalThis.queueMicrotask(poll);
|
|
170
|
+
};
|
|
171
|
+
poll();
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
const calculateOptimalBatchSize = (measuredTimePerRowMs, targetTimePerBatchMs = 5000, minBatchSize = 100, maxBatchSize = 3000) => {
|
|
175
|
+
if (measuredTimePerRowMs <= 0) {
|
|
176
|
+
return minBatchSize;
|
|
177
|
+
}
|
|
178
|
+
const calculatedSize = Math.floor(targetTimePerBatchMs / measuredTimePerRowMs);
|
|
179
|
+
return Math.max(minBatchSize, Math.min(maxBatchSize, calculatedSize));
|
|
180
|
+
};
|
|
181
|
+
const getSourceBatchTuning = (connection, initialBatchSize) => {
|
|
182
|
+
if (connection.sourceBatchTuning !== undefined) {
|
|
183
|
+
return connection.sourceBatchTuning;
|
|
184
|
+
}
|
|
185
|
+
const startingBatchSize = Math.max(MIN_SOURCE_READ_BATCH_SIZE, Math.min(DEFAULT_SOURCE_READ_BATCH_SIZE, initialBatchSize));
|
|
186
|
+
connection.sourceBatchTuning = {
|
|
187
|
+
readBatchSize: startingBatchSize,
|
|
188
|
+
readBatchHistory: [],
|
|
189
|
+
};
|
|
190
|
+
return connection.sourceBatchTuning;
|
|
191
|
+
};
|
|
192
|
+
const isRetryableConnectionError = (error) => {
|
|
193
|
+
const message = getErrorMessage(error).toLowerCase();
|
|
194
|
+
return (message.includes('etimedout') ||
|
|
195
|
+
message.includes('timed out') ||
|
|
196
|
+
message.includes('timeout') ||
|
|
197
|
+
message.includes('econnreset') ||
|
|
198
|
+
message.includes('connection lost') ||
|
|
199
|
+
message.includes('server has gone away') ||
|
|
200
|
+
message.includes('read timeout') ||
|
|
201
|
+
message.includes('write timeout') ||
|
|
202
|
+
message.includes('wait_timeout'));
|
|
203
|
+
};
|
|
204
|
+
const connectWithRetry = async (adapter, label) => {
|
|
205
|
+
let lastError;
|
|
206
|
+
for (let attempt = 0; attempt < SOURCE_CONNECT_RETRY_ATTEMPTS; attempt += 1) {
|
|
207
|
+
try {
|
|
208
|
+
await adapter.connect();
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
lastError = error;
|
|
213
|
+
const retryable = isRetryableConnectionError(error);
|
|
214
|
+
const isLastAttempt = attempt >= SOURCE_CONNECT_RETRY_ATTEMPTS - 1;
|
|
215
|
+
if (!retryable || isLastAttempt) {
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
const delayMs = SOURCE_CONNECT_RETRY_BASE_DELAY_MS * 2 ** attempt;
|
|
219
|
+
Logger.warn(`[DataMigrator] ${label} connect failed on attempt ${attempt + 1}/${SOURCE_CONNECT_RETRY_ATTEMPTS}, retrying in ${delayMs}ms: ${getErrorMessage(error)}`);
|
|
220
|
+
await waitMs(delayMs);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
throw lastError;
|
|
224
|
+
};
|
|
225
|
+
const updateSourceBatchTuningAfterSuccess = (connection, rowsRead, durationMs, batchSize) => {
|
|
226
|
+
const tuning = getSourceBatchTuning(connection, batchSize);
|
|
227
|
+
const timePerRow = rowsRead > 0 ? durationMs / rowsRead : durationMs;
|
|
228
|
+
tuning.readBatchHistory.push({ rowsRead, durationMs, timePerRow });
|
|
229
|
+
if (tuning.readBatchHistory.length > 10) {
|
|
230
|
+
tuning.readBatchHistory.shift();
|
|
231
|
+
}
|
|
232
|
+
const avgTimePerRow = tuning.readBatchHistory.reduce((sum, entry) => sum + entry.timePerRow, 0) /
|
|
233
|
+
tuning.readBatchHistory.length;
|
|
234
|
+
const targetTimePerBatchMs = Math.max(1000, Math.min(SOURCE_READ_TARGET_TIME_MS, Math.floor(batchSize * 5)));
|
|
235
|
+
const optimalSize = calculateOptimalBatchSize(avgTimePerRow, targetTimePerBatchMs, MIN_SOURCE_READ_BATCH_SIZE, MAX_SOURCE_READ_BATCH_SIZE);
|
|
236
|
+
if (Math.abs(optimalSize - tuning.readBatchSize) >= 50) {
|
|
237
|
+
tuning.readBatchSize = optimalSize;
|
|
238
|
+
Logger.info(`[DataMigrator] Adaptive source batching: adjusted to ${optimalSize} rows (avg ${avgTimePerRow.toFixed(2)}ms/row)`);
|
|
239
|
+
}
|
|
240
|
+
};
|
|
241
|
+
const reduceSourceBatchSizeAfterTimeout = (connection, batchSize) => {
|
|
242
|
+
const tuning = getSourceBatchTuning(connection, batchSize);
|
|
243
|
+
const reducedSize = Math.max(MIN_SOURCE_READ_BATCH_SIZE, Math.floor(Math.min(tuning.readBatchSize, batchSize) / 2));
|
|
244
|
+
if (reducedSize < tuning.readBatchSize) {
|
|
245
|
+
tuning.readBatchSize = reducedSize;
|
|
246
|
+
Logger.warn(`[DataMigrator] Adaptive source batching: reduced to ${reducedSize} rows after timeout-like failure`);
|
|
247
|
+
}
|
|
248
|
+
return reducedSize;
|
|
249
|
+
};
|
|
250
|
+
const getRemoteBatchTuning = (connection) => {
|
|
251
|
+
if (connection.remoteBatchTuning !== undefined) {
|
|
252
|
+
return connection.remoteBatchTuning;
|
|
253
|
+
}
|
|
254
|
+
connection.remoteBatchTuning = {
|
|
255
|
+
rowsPerStatement: REMOTE_INSERT_ROWS_PER_STATEMENT,
|
|
256
|
+
maxStatementSqlLength: MAX_REMOTE_INSERT_SQL_LENGTH,
|
|
257
|
+
maxExecutionSqlLength: MAX_REMOTE_EXECUTION_SQL_LENGTH,
|
|
258
|
+
};
|
|
259
|
+
return connection.remoteBatchTuning;
|
|
260
|
+
};
|
|
261
|
+
const getInsertBatchSettings = (connection) => {
|
|
262
|
+
if (connection.type === 'd1-remote') {
|
|
263
|
+
const tuning = getRemoteBatchTuning(connection);
|
|
264
|
+
return {
|
|
265
|
+
rowsPerStatement: tuning.rowsPerStatement,
|
|
266
|
+
maxStatementSqlLength: tuning.maxStatementSqlLength,
|
|
267
|
+
maxExecutionSqlLength: tuning.maxExecutionSqlLength,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
rowsPerStatement: LOCAL_INSERT_ROWS_PER_STATEMENT,
|
|
272
|
+
maxStatementSqlLength: Number.POSITIVE_INFINITY,
|
|
273
|
+
maxExecutionSqlLength: Number.POSITIVE_INFINITY,
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
const adjustRemoteBatchTuning = (connection, executedRows, durationMs, sqlLength) => {
|
|
277
|
+
if (connection.type !== 'd1-remote' || executedRows <= 0) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const tuning = getRemoteBatchTuning(connection);
|
|
281
|
+
const previousRowsPerStatement = tuning.rowsPerStatement;
|
|
282
|
+
const nearSqlLimit = sqlLength >= Math.floor(tuning.maxExecutionSqlLength * 0.9);
|
|
283
|
+
if (durationMs <= 2500 &&
|
|
284
|
+
executedRows >= Math.floor(previousRowsPerStatement * 0.8) &&
|
|
285
|
+
!nearSqlLimit) {
|
|
286
|
+
tuning.rowsPerStatement = Math.min(MAX_REMOTE_INSERT_ROWS_PER_STATEMENT, Math.max(previousRowsPerStatement + 50, Math.floor(previousRowsPerStatement * 1.3)));
|
|
287
|
+
}
|
|
288
|
+
else if (durationMs >= 10000 || nearSqlLimit) {
|
|
289
|
+
tuning.rowsPerStatement = Math.max(MIN_REMOTE_INSERT_ROWS_PER_STATEMENT, Math.min(previousRowsPerStatement - 50, Math.floor(previousRowsPerStatement * 0.8)));
|
|
290
|
+
}
|
|
291
|
+
if (tuning.rowsPerStatement !== previousRowsPerStatement) {
|
|
292
|
+
Logger.info(`[DataMigrator] Adaptive remote batching: rows_per_statement ${previousRowsPerStatement} -> ${tuning.rowsPerStatement} after ${executedRows} rows in ${formatDuration(durationMs)}`);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
const reduceRemoteBatchTuningAfterFailure = (connection, sqlLength) => {
|
|
296
|
+
if (connection.type !== 'd1-remote') {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const tuning = getRemoteBatchTuning(connection);
|
|
300
|
+
const previousRowsPerStatement = tuning.rowsPerStatement;
|
|
301
|
+
const nearSqlLimit = sqlLength >= Math.floor(tuning.maxExecutionSqlLength * 0.8);
|
|
302
|
+
if (!nearSqlLimit && previousRowsPerStatement <= MIN_REMOTE_INSERT_ROWS_PER_STATEMENT) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
tuning.rowsPerStatement = Math.max(MIN_REMOTE_INSERT_ROWS_PER_STATEMENT, Math.min(previousRowsPerStatement - 50, Math.floor(previousRowsPerStatement * 0.5)));
|
|
306
|
+
if (tuning.rowsPerStatement !== previousRowsPerStatement) {
|
|
307
|
+
Logger.warn(`[DataMigrator] Adaptive remote batching: rows_per_statement ${previousRowsPerStatement} -> ${tuning.rowsPerStatement} after failed remote insert`);
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
const logTableMigrationProgress = (table, rowsMigrated, totalRows, batchSize) => {
|
|
311
|
+
if (totalRows <= 10000 || rowsMigrated % (batchSize * 10) !== 0) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const normalizedTotalRows = Math.max(totalRows, rowsMigrated);
|
|
315
|
+
const percentage = Math.round((rowsMigrated / normalizedTotalRows) * 100);
|
|
316
|
+
Logger.info(`Table ${table.name}: ${rowsMigrated}/${normalizedTotalRows} (${percentage}%)`);
|
|
317
|
+
};
|
|
318
|
+
const getTableChunkBatchSize = (sourceConnection, retryBatchSize) => {
|
|
319
|
+
if (sourceConnection.driver !== 'mysql') {
|
|
320
|
+
return retryBatchSize;
|
|
321
|
+
}
|
|
322
|
+
return getSourceBatchTuning(sourceConnection, retryBatchSize).readBatchSize;
|
|
323
|
+
};
|
|
324
|
+
const applyTableChunkResult = (state, result, initialBatchSize, table, totalRows) => {
|
|
325
|
+
const nextState = {
|
|
326
|
+
rowsMigrated: state.rowsMigrated + result.rowsMigrated,
|
|
327
|
+
offset: result.nextOffset,
|
|
328
|
+
currentBatchSize: result.nextBatchSize,
|
|
329
|
+
retryBatchSize: result.nextBatchSize,
|
|
330
|
+
};
|
|
331
|
+
logTableMigrationProgress(table, nextState.rowsMigrated, totalRows, nextState.currentBatchSize);
|
|
332
|
+
if (!result.continueProcessing &&
|
|
333
|
+
result.rowsMigrated === 0 &&
|
|
334
|
+
result.nextBatchSize < initialBatchSize) {
|
|
335
|
+
return nextState;
|
|
336
|
+
}
|
|
337
|
+
return nextState;
|
|
338
|
+
};
|
|
339
|
+
const verifyChunkInsertCount = (tableName, offset, expectedRows, insertedRows) => {
|
|
340
|
+
if (insertedRows === expectedRows) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const verificationError = DataMigrator.createChunkVerificationError(tableName, offset, expectedRows, insertedRows);
|
|
344
|
+
throw ErrorFactory.createValidationError(`Chunk insert mismatch on ${tableName}`, verificationError);
|
|
345
|
+
};
|
|
346
|
+
const logChunkProcessingSuccess = (tableName, offset, insertedRows, chunkDurationMs) => {
|
|
347
|
+
Logger.info(`[DataMigrator] Chunk ${tableName} offset=${offset} rows=${insertedRows} duration=${formatDuration(chunkDurationMs)} rate=${formatRowsPerSecond(insertedRows, chunkDurationMs)}`);
|
|
348
|
+
};
|
|
349
|
+
const executeChunkRows = async (context) => {
|
|
350
|
+
const chunk = await DataMigrator.readDataChunk(context.sourceConnection, context.table.name, context.offset, context.batchSize);
|
|
351
|
+
if (chunk.length === 0) {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
const transformedChunk = await DataMigrator.transformData(chunk, context.table.name);
|
|
355
|
+
const insertedRows = await DataMigrator.insertData(context.targetConnection, context.table.name, transformedChunk);
|
|
356
|
+
verifyChunkInsertCount(context.table.name, context.offset, chunk.length, insertedRows);
|
|
357
|
+
return {
|
|
358
|
+
chunkLength: chunk.length,
|
|
359
|
+
insertedRows,
|
|
360
|
+
};
|
|
361
|
+
};
|
|
362
|
+
const createEmptyChunkResult = (offset, batchSize) => {
|
|
363
|
+
return {
|
|
364
|
+
rowsMigrated: 0,
|
|
365
|
+
nextOffset: offset,
|
|
366
|
+
nextBatchSize: batchSize,
|
|
367
|
+
continueProcessing: false,
|
|
368
|
+
};
|
|
369
|
+
};
|
|
370
|
+
const buildChunkProcessingSuccessResult = async (table, sourceConnection, targetConnection, offset, batchSize) => {
|
|
371
|
+
const chunkStartTime = Date.now();
|
|
372
|
+
const executionResult = await executeChunkRows({
|
|
373
|
+
table,
|
|
374
|
+
sourceConnection,
|
|
375
|
+
targetConnection,
|
|
376
|
+
offset,
|
|
377
|
+
batchSize,
|
|
378
|
+
});
|
|
379
|
+
if (executionResult === null) {
|
|
380
|
+
return createEmptyChunkResult(offset, batchSize);
|
|
381
|
+
}
|
|
382
|
+
const { chunkLength, insertedRows } = executionResult;
|
|
383
|
+
const chunkDurationMs = Date.now() - chunkStartTime;
|
|
384
|
+
if (sourceConnection.driver === 'mysql') {
|
|
385
|
+
updateSourceBatchTuningAfterSuccess(sourceConnection, insertedRows, chunkDurationMs, batchSize);
|
|
386
|
+
}
|
|
387
|
+
logChunkProcessingSuccess(table.name, offset, insertedRows, chunkDurationMs);
|
|
388
|
+
if (chunkLength < batchSize) {
|
|
389
|
+
return {
|
|
390
|
+
rowsMigrated: insertedRows,
|
|
391
|
+
nextOffset: offset + chunkLength,
|
|
392
|
+
nextBatchSize: batchSize,
|
|
393
|
+
continueProcessing: false,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
rowsMigrated: insertedRows,
|
|
398
|
+
nextOffset: offset + batchSize,
|
|
399
|
+
nextBatchSize: batchSize,
|
|
400
|
+
continueProcessing: true,
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
const buildChunkProcessingFailureResult = (sourceConnection, offset, batchSize, error, errors) => {
|
|
404
|
+
const errorMessage = getErrorMessage(error);
|
|
405
|
+
const errorMsg = `Chunk processing failed at offset ${offset}: ${errorMessage}`;
|
|
406
|
+
Logger.warn(errorMsg);
|
|
407
|
+
errors.push(errorMsg);
|
|
408
|
+
if (sourceConnection.driver === 'mysql' && isRetryableConnectionError(error)) {
|
|
409
|
+
const reducedBatchSize = reduceSourceBatchSizeAfterTimeout(sourceConnection, batchSize);
|
|
410
|
+
return {
|
|
411
|
+
rowsMigrated: 0,
|
|
412
|
+
nextOffset: offset,
|
|
413
|
+
nextBatchSize: reducedBatchSize,
|
|
414
|
+
continueProcessing: reducedBatchSize >= batchSize,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
rowsMigrated: 0,
|
|
419
|
+
nextOffset: offset + batchSize,
|
|
420
|
+
nextBatchSize: batchSize,
|
|
421
|
+
continueProcessing: true,
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
const processTableChunk = async (table, sourceConnection, targetConnection, offset, batchSize, errors) => {
|
|
425
|
+
try {
|
|
426
|
+
return await buildChunkProcessingSuccessResult(table, sourceConnection, targetConnection, offset, batchSize);
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
return buildChunkProcessingFailureResult(sourceConnection, offset, batchSize, error, errors);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
const getTableDependencies = (table) => {
|
|
433
|
+
if (!Array.isArray(table.dependsOn)) {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
return [...new Set(table.dependsOn.filter((dependency) => dependency.trim() !== ''))];
|
|
437
|
+
};
|
|
438
|
+
const buildTableMigrationLevels = (tables) => {
|
|
439
|
+
const tablesByName = new Map();
|
|
440
|
+
for (const table of tables) {
|
|
441
|
+
tablesByName.set(table.name, table);
|
|
442
|
+
}
|
|
443
|
+
const unresolved = new Set(tables.map((table) => table.name));
|
|
444
|
+
const dependenciesByTable = new Map();
|
|
445
|
+
for (const table of tables) {
|
|
446
|
+
dependenciesByTable.set(table.name, new Set(getTableDependencies(table).filter((dependency) => dependency !== table.name && tablesByName.has(dependency))));
|
|
447
|
+
}
|
|
448
|
+
const levels = [];
|
|
449
|
+
while (unresolved.size > 0) {
|
|
450
|
+
const readyNames = tables
|
|
451
|
+
.map((table) => table.name)
|
|
452
|
+
.filter((name) => unresolved.has(name) && (dependenciesByTable.get(name)?.size ?? 0) === 0);
|
|
453
|
+
if (readyNames.length === 0) {
|
|
454
|
+
const cyclicTables = tables.filter((table) => unresolved.has(table.name));
|
|
455
|
+
if (cyclicTables.length > 0) {
|
|
456
|
+
Logger.warn(`[DataMigrator] Table dependency cycle or unresolved reference detected. Falling back to sequential execution for: ${cyclicTables.map((table) => table.name).join(', ')}`);
|
|
457
|
+
levels.push(...cyclicTables.map((table) => [table]));
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
const levelTables = readyNames
|
|
462
|
+
.map((name) => tablesByName.get(name))
|
|
463
|
+
.filter((table) => table !== undefined);
|
|
464
|
+
levels.push(levelTables);
|
|
465
|
+
for (const readyName of readyNames) {
|
|
466
|
+
unresolved.delete(readyName);
|
|
467
|
+
}
|
|
468
|
+
for (const name of unresolved) {
|
|
469
|
+
const dependencies = dependenciesByTable.get(name);
|
|
470
|
+
for (const readyName of readyNames) {
|
|
471
|
+
dependencies?.delete(readyName);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
return levels;
|
|
476
|
+
};
|
|
477
|
+
const getTableParallelism = (config, targetConnection) => {
|
|
478
|
+
if (targetConnection.type !== 'd1-remote') {
|
|
479
|
+
return 1;
|
|
480
|
+
}
|
|
481
|
+
if (config.sourceDriver === 'sqlite') {
|
|
482
|
+
return 1;
|
|
483
|
+
}
|
|
484
|
+
return DEFAULT_REMOTE_TABLE_PARALLELISM;
|
|
485
|
+
};
|
|
486
|
+
const executeWithConcurrency = async (items, concurrency, worker) => {
|
|
487
|
+
if (items.length === 0) {
|
|
488
|
+
return [];
|
|
489
|
+
}
|
|
490
|
+
const effectiveConcurrency = Math.max(1, Math.min(concurrency, items.length));
|
|
491
|
+
const results = new Array(items.length);
|
|
492
|
+
let index = 0;
|
|
493
|
+
const runWorker = async () => {
|
|
494
|
+
while (index < items.length) {
|
|
495
|
+
const currentIndex = index;
|
|
496
|
+
index += 1;
|
|
497
|
+
results[currentIndex] = await worker(items[currentIndex]);
|
|
498
|
+
}
|
|
499
|
+
};
|
|
500
|
+
await Promise.all(Array.from({ length: effectiveConcurrency }, () => runWorker()));
|
|
501
|
+
return results;
|
|
502
|
+
};
|
|
503
|
+
const estimateRemoteRowSqlLength = (keys, row) => {
|
|
504
|
+
const delimitersLength = keys.length > 0 ? (keys.length - 1) * 2 : 0;
|
|
505
|
+
const valuesLength = keys.reduce((total, key) => {
|
|
506
|
+
return total + toSqlLiteral(row[key]).length;
|
|
507
|
+
}, 0);
|
|
508
|
+
return valuesLength + delimitersLength + 2;
|
|
509
|
+
};
|
|
510
|
+
const createInsertStatements = (targetType, settings, tableName, data) => {
|
|
511
|
+
if (data.length === 0) {
|
|
512
|
+
return [];
|
|
513
|
+
}
|
|
514
|
+
const keys = Object.keys(data[0]);
|
|
515
|
+
const columnList = keys.map((key) => `\`${key}\``).join(', ');
|
|
516
|
+
const rowPlaceholder = `(${keys.map(() => '?').join(', ')})`;
|
|
517
|
+
const prefix = `INSERT INTO \`${tableName}\` (${columnList}) VALUES `;
|
|
518
|
+
const rowLimit = settings.rowsPerStatement;
|
|
519
|
+
const maxSqlLength = settings.maxStatementSqlLength;
|
|
520
|
+
const statements = [];
|
|
521
|
+
let batchRows = [];
|
|
522
|
+
let batchParameters = [];
|
|
523
|
+
let batchSqlLength = prefix.length;
|
|
524
|
+
const flushBatch = () => {
|
|
525
|
+
if (batchRows.length === 0) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
statements.push({
|
|
529
|
+
sql: `${prefix}${batchRows.map(() => rowPlaceholder).join(', ')}`,
|
|
530
|
+
parameters: batchParameters,
|
|
531
|
+
rowCount: batchRows.length,
|
|
532
|
+
});
|
|
533
|
+
batchRows = [];
|
|
534
|
+
batchParameters = [];
|
|
535
|
+
batchSqlLength = prefix.length;
|
|
536
|
+
};
|
|
537
|
+
for (const row of data) {
|
|
538
|
+
const rowParameters = keys.map((key) => row[key]);
|
|
539
|
+
const rowSqlLength = targetType === 'd1-remote' ? estimateRemoteRowSqlLength(keys, row) : rowPlaceholder.length;
|
|
540
|
+
const separatorLength = batchRows.length > 0 ? 2 : 0;
|
|
541
|
+
const nextSqlLength = batchSqlLength + separatorLength + rowSqlLength;
|
|
542
|
+
if (batchRows.length > 0 && (batchRows.length >= rowLimit || nextSqlLength > maxSqlLength)) {
|
|
543
|
+
flushBatch();
|
|
544
|
+
}
|
|
545
|
+
batchRows.push(row);
|
|
546
|
+
batchParameters.push(...rowParameters);
|
|
547
|
+
batchSqlLength += (batchRows.length > 1 ? 2 : 0) + rowSqlLength;
|
|
548
|
+
}
|
|
549
|
+
flushBatch();
|
|
550
|
+
return statements;
|
|
551
|
+
};
|
|
552
|
+
const createRemoteExecutionBatchesWithLimit = (statements, maxExecutionSqlLength) => {
|
|
553
|
+
if (statements.length <= 1) {
|
|
554
|
+
return statements;
|
|
555
|
+
}
|
|
556
|
+
const batches = [];
|
|
557
|
+
let sqlParts = [];
|
|
558
|
+
let parameters = [];
|
|
559
|
+
let rowCount = 0;
|
|
560
|
+
let currentLength = 0;
|
|
561
|
+
const flushBatch = () => {
|
|
562
|
+
if (sqlParts.length === 0) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
batches.push({
|
|
566
|
+
sql: sqlParts.join(';\n'),
|
|
567
|
+
parameters,
|
|
568
|
+
rowCount,
|
|
569
|
+
});
|
|
570
|
+
sqlParts = [];
|
|
571
|
+
parameters = [];
|
|
572
|
+
rowCount = 0;
|
|
573
|
+
currentLength = 0;
|
|
574
|
+
};
|
|
575
|
+
for (const statement of statements) {
|
|
576
|
+
const separatorLength = sqlParts.length > 0 ? 2 : 0;
|
|
577
|
+
const nextLength = currentLength + separatorLength + statement.sql.length;
|
|
578
|
+
if (sqlParts.length > 0 && nextLength > maxExecutionSqlLength) {
|
|
579
|
+
flushBatch();
|
|
580
|
+
}
|
|
581
|
+
sqlParts.push(statement.sql);
|
|
582
|
+
parameters.push(...statement.parameters);
|
|
583
|
+
rowCount += statement.rowCount;
|
|
584
|
+
currentLength += (sqlParts.length > 1 ? 2 : 0) + statement.sql.length;
|
|
585
|
+
}
|
|
586
|
+
flushBatch();
|
|
587
|
+
return batches;
|
|
588
|
+
};
|
|
97
589
|
const createRemoteD1Adapter = (database) => {
|
|
98
590
|
return {
|
|
99
591
|
async connect() {
|
|
@@ -107,38 +599,22 @@ const createRemoteD1Adapter = (database) => {
|
|
|
107
599
|
const output = WranglerD1.executeSql({ dbName: database, isLocal: false, sql: renderedSql });
|
|
108
600
|
const payload = extractWranglerJson(output);
|
|
109
601
|
if (payload === null || payload.length === 0) {
|
|
110
|
-
const rows =
|
|
602
|
+
const rows = parseWranglerTableRows(output);
|
|
111
603
|
return { rows, rowCount: rows.length };
|
|
112
604
|
}
|
|
113
|
-
const last = payload
|
|
605
|
+
const last = payload.at(-1);
|
|
606
|
+
if (last === undefined) {
|
|
607
|
+
return { rows: [], rowCount: 0 };
|
|
608
|
+
}
|
|
114
609
|
const rows = Array.isArray(last.results) ? last.results : [];
|
|
115
|
-
const
|
|
116
|
-
|
|
610
|
+
const totalChanges = payload.reduce((count, statement) => {
|
|
611
|
+
return count + (typeof statement.meta?.changes === 'number' ? statement.meta.changes : 0);
|
|
612
|
+
}, 0);
|
|
613
|
+
const rowCount = totalChanges > 0 ? totalChanges : rows.length;
|
|
117
614
|
return { rows, rowCount };
|
|
118
615
|
},
|
|
119
616
|
};
|
|
120
617
|
};
|
|
121
|
-
const redactConnectionString = (connectionString) => {
|
|
122
|
-
try {
|
|
123
|
-
const parsed = new URL(connectionString);
|
|
124
|
-
if (parsed.password.trim() !== '') {
|
|
125
|
-
parsed.password = '***';
|
|
126
|
-
}
|
|
127
|
-
return parsed.toString();
|
|
128
|
-
}
|
|
129
|
-
catch {
|
|
130
|
-
return connectionString;
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
const getUrlPasswordForm = (connectionString) => {
|
|
134
|
-
try {
|
|
135
|
-
const parsed = new URL(connectionString);
|
|
136
|
-
return parsed.password || '';
|
|
137
|
-
}
|
|
138
|
-
catch {
|
|
139
|
-
return undefined;
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
618
|
const getErrorCause = (error) => {
|
|
143
619
|
if (error === null || typeof error !== 'object') {
|
|
144
620
|
return undefined;
|
|
@@ -182,19 +658,11 @@ const logDetailedError = (label, error) => {
|
|
|
182
658
|
if (driverDetails !== undefined) {
|
|
183
659
|
Logger.error(`${label} driver details: ${driverDetails}`);
|
|
184
660
|
}
|
|
185
|
-
if (error instanceof Error && typeof error.stack === 'string' && error.stack.trim() !== '') {
|
|
186
|
-
Logger.error(`${label} stack: ${error.stack}`);
|
|
187
|
-
}
|
|
188
661
|
const cause = getErrorCause(error);
|
|
189
662
|
if (cause !== undefined) {
|
|
190
663
|
logDetailedError(`${label} cause`, cause);
|
|
191
664
|
}
|
|
192
665
|
};
|
|
193
|
-
const describePasswordForLog = (password) => {
|
|
194
|
-
const hasSpecialCharacters = /[^a-zA-Z0-9]/.test(password);
|
|
195
|
-
const containsBang = password.includes('!');
|
|
196
|
-
return `len=${password.length}, special_chars=${hasSpecialCharacters}, contains_bang=${containsBang}`;
|
|
197
|
-
};
|
|
198
666
|
const normalizeNullLikeValue = (value) => {
|
|
199
667
|
if (typeof value !== 'string')
|
|
200
668
|
return value;
|
|
@@ -240,11 +708,6 @@ const createSourceAdapter = (config) => {
|
|
|
240
708
|
switch (config.sourceDriver) {
|
|
241
709
|
case 'mysql': {
|
|
242
710
|
const connectionDetails = parseConnectionDetails(config.sourceConnection, 3306, 'mysql', 'root');
|
|
243
|
-
const urlPasswordForm = getUrlPasswordForm(config.sourceConnection);
|
|
244
|
-
Logger.info(`[DataMigrator] Source password diagnostics: ${describePasswordForLog(connectionDetails.password)}`);
|
|
245
|
-
if (urlPasswordForm !== undefined) {
|
|
246
|
-
Logger.info(`[DataMigrator] MySQL password handoff: url_form(${describePasswordForLog(urlPasswordForm)}), final_auth(${describePasswordForLog(connectionDetails.password)}), matches=${urlPasswordForm === connectionDetails.password}`);
|
|
247
|
-
}
|
|
248
711
|
return MySQLAdapter.create({
|
|
249
712
|
driver: 'mysql',
|
|
250
713
|
host: connectionDetails.host,
|
|
@@ -253,6 +716,14 @@ const createSourceAdapter = (config) => {
|
|
|
253
716
|
username: connectionDetails.username,
|
|
254
717
|
password: connectionDetails.password,
|
|
255
718
|
ssl: config.sourceSsl,
|
|
719
|
+
socketTimeoutMs: config.sourceSocketTimeoutMs,
|
|
720
|
+
connectTimeoutMs: Math.max(config.sourceSocketTimeoutMs ?? 30000, 60000),
|
|
721
|
+
acquireTimeoutMs: Math.max(config.sourceSocketTimeoutMs ?? 30000, 60000),
|
|
722
|
+
enableKeepAlive: true,
|
|
723
|
+
keepAliveInitialDelayMs: 0,
|
|
724
|
+
waitTimeoutSeconds: config.sourceWaitTimeoutSeconds,
|
|
725
|
+
netReadTimeoutSeconds: config.sourceNetReadTimeoutSeconds,
|
|
726
|
+
netWriteTimeoutSeconds: config.sourceNetWriteTimeoutSeconds,
|
|
256
727
|
});
|
|
257
728
|
}
|
|
258
729
|
case 'postgresql': {
|
|
@@ -335,14 +806,20 @@ export const DataMigrator = Object.freeze({
|
|
|
335
806
|
if (targetConnection.adapter) {
|
|
336
807
|
await DataMigrator.prepareTargetSchema(sourceConnection, targetConnection, config);
|
|
337
808
|
}
|
|
338
|
-
// Migrate each table sequentially for reliable D1/SQLite writes
|
|
339
809
|
Logger.info('Starting table migration...');
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
810
|
+
const tableLevels = buildTableMigrationLevels(schema.tables);
|
|
811
|
+
const tableParallelism = getTableParallelism(config, targetConnection);
|
|
812
|
+
for (const [levelIndex, tables] of tableLevels.entries()) {
|
|
813
|
+
Logger.info(`[DataMigrator] Starting table level ${levelIndex + 1}/${tableLevels.length}: ${tables.map((table) => table.name).join(', ')}`);
|
|
814
|
+
const levelResults = await executeWithConcurrency(tables, tableParallelism, async (table) => {
|
|
815
|
+
return DataMigrator.migrateTable(table, sourceConnection, targetConnection, config);
|
|
816
|
+
});
|
|
817
|
+
for (const [resultIndex, result] of levelResults.entries()) {
|
|
818
|
+
const table = tables[resultIndex];
|
|
819
|
+
progress.processedRows += result.rowsMigrated;
|
|
820
|
+
if (result.errors.length > 0 && table !== undefined) {
|
|
821
|
+
progress.errors[table.name] = result.errors.join('; ');
|
|
822
|
+
}
|
|
346
823
|
}
|
|
347
824
|
}
|
|
348
825
|
progress.totalRows = Math.max(progress.totalRows, progress.processedRows);
|
|
@@ -359,7 +836,6 @@ export const DataMigrator = Object.freeze({
|
|
|
359
836
|
const errorChain = getErrorChainMessages(error);
|
|
360
837
|
Logger.error(`Data migration failed: ${errorChain.join(' -> ')}`);
|
|
361
838
|
logDetailedError('Data migration failure', error);
|
|
362
|
-
Logger.error('Data migration failure details:', error);
|
|
363
839
|
throw error;
|
|
364
840
|
}
|
|
365
841
|
finally {
|
|
@@ -372,16 +848,14 @@ export const DataMigrator = Object.freeze({
|
|
|
372
848
|
*/
|
|
373
849
|
async connectToSource(config) {
|
|
374
850
|
Logger.info(`Connecting to ${config.sourceDriver} database...`);
|
|
375
|
-
Logger.info(`[DataMigrator] Source connection (redacted): ${redactConnectionString(config.sourceConnection)}`);
|
|
376
851
|
const adapter = createSourceAdapter(config);
|
|
377
852
|
try {
|
|
378
|
-
await adapter.
|
|
853
|
+
await connectWithRetry(adapter, config.sourceDriver);
|
|
379
854
|
}
|
|
380
855
|
catch (error) {
|
|
381
856
|
const errorChain = getErrorChainMessages(error);
|
|
382
857
|
Logger.error(`Source database connection failed: ${errorChain.join(' -> ')}`);
|
|
383
858
|
logDetailedError('Source database connection failure', error);
|
|
384
|
-
Logger.error('Source database connection failure details:', error);
|
|
385
859
|
throw error;
|
|
386
860
|
}
|
|
387
861
|
const sourceConnection = {
|
|
@@ -393,6 +867,9 @@ export const DataMigrator = Object.freeze({
|
|
|
393
867
|
adapter,
|
|
394
868
|
};
|
|
395
869
|
Logger.info('✓ Source database connected');
|
|
870
|
+
if (config.sourceDriver === 'mysql') {
|
|
871
|
+
sourceConnection.sourceBatchTuning = getSourceBatchTuning(sourceConnection, config.batchSize ?? DEFAULT_SOURCE_READ_BATCH_SIZE);
|
|
872
|
+
}
|
|
396
873
|
return sourceConnection;
|
|
397
874
|
},
|
|
398
875
|
/**
|
|
@@ -438,6 +915,7 @@ export const DataMigrator = Object.freeze({
|
|
|
438
915
|
if (!targetConnection.adapter) {
|
|
439
916
|
throw ErrorFactory.createConnectionError('No target adapter available for D1 schema preparation');
|
|
440
917
|
}
|
|
918
|
+
const adapter = targetConnection.adapter;
|
|
441
919
|
Logger.info('Preparing target D1 schema...');
|
|
442
920
|
const sourceSchema = await SchemaAnalyzer.analyzeSchema({
|
|
443
921
|
driver: sourceConnection.driver,
|
|
@@ -447,16 +925,42 @@ export const DataMigrator = Object.freeze({
|
|
|
447
925
|
});
|
|
448
926
|
const d1Schema = SchemaBuilder.buildD1Schema(sourceSchema.tables, config.sourceDriver);
|
|
449
927
|
SchemaBuilder.assertValidSchema(d1Schema);
|
|
928
|
+
const schemaStatements = [];
|
|
929
|
+
const maxBatchSqlLength = targetConnection.type === 'd1-remote'
|
|
930
|
+
? Math.max(0, MAX_REMOTE_EXECUTION_SQL_LENGTH - 2000)
|
|
931
|
+
: Number.POSITIVE_INFINITY;
|
|
932
|
+
const executeSchemaBatch = async () => {
|
|
933
|
+
if (schemaStatements.length === 0) {
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
936
|
+
const batchSql = `${schemaStatements.join(';\n')};`;
|
|
937
|
+
await adapter.query(batchSql, []);
|
|
938
|
+
schemaStatements.length = 0;
|
|
939
|
+
};
|
|
940
|
+
const pushSchemaStatement = async (sql) => {
|
|
941
|
+
const trimmedSql = sql.trim();
|
|
942
|
+
const nextLength = schemaStatements.length === 0
|
|
943
|
+
? trimmedSql.length
|
|
944
|
+
: schemaStatements.join(';\n').length + 2 + trimmedSql.length;
|
|
945
|
+
if (targetConnection.type === 'd1-remote' && nextLength > maxBatchSqlLength) {
|
|
946
|
+
await executeSchemaBatch();
|
|
947
|
+
}
|
|
948
|
+
schemaStatements.push(trimmedSql);
|
|
949
|
+
};
|
|
450
950
|
for (const table of d1Schema) {
|
|
451
951
|
const createSQL = SchemaBuilder.generateCreateTableSQL(table).replace(/^CREATE TABLE\s+/i, 'CREATE TABLE IF NOT EXISTS ');
|
|
452
|
-
await
|
|
952
|
+
await pushSchemaStatement(createSQL);
|
|
453
953
|
const indexSQL = SchemaBuilder.generateIndexSQL(table).map((sql) => sql
|
|
454
954
|
.replace(/^CREATE\s+UNIQUE\s+INDEX\s+/i, 'CREATE UNIQUE INDEX IF NOT EXISTS ')
|
|
455
955
|
.replace(/^CREATE\s+INDEX\s+/i, 'CREATE INDEX IF NOT EXISTS '));
|
|
456
956
|
for (const sql of indexSQL) {
|
|
457
|
-
await
|
|
957
|
+
await pushSchemaStatement(sql);
|
|
958
|
+
}
|
|
959
|
+
if (targetConnection.type !== 'd1-remote') {
|
|
960
|
+
await executeSchemaBatch();
|
|
458
961
|
}
|
|
459
962
|
}
|
|
963
|
+
await executeSchemaBatch();
|
|
460
964
|
Logger.info(`✓ Target schema prepared for ${d1Schema.length} tables`);
|
|
461
965
|
},
|
|
462
966
|
/**
|
|
@@ -473,6 +977,7 @@ export const DataMigrator = Object.freeze({
|
|
|
473
977
|
const tables = sourceSchema.tables.map((table) => ({
|
|
474
978
|
name: table.name,
|
|
475
979
|
rowCount: table.rowCount || 0,
|
|
980
|
+
dependsOn: table.foreignKeys.map((foreignKey) => foreignKey.referencedTable),
|
|
476
981
|
}));
|
|
477
982
|
Logger.info(`Found ${tables.length} tables`);
|
|
478
983
|
return { tables };
|
|
@@ -500,10 +1005,10 @@ export const DataMigrator = Object.freeze({
|
|
|
500
1005
|
Logger.info(`Migrating table: ${table.name}`);
|
|
501
1006
|
const errors = [];
|
|
502
1007
|
let rowsMigrated = 0;
|
|
1008
|
+
const tableStartTime = Date.now();
|
|
503
1009
|
try {
|
|
504
1010
|
const totalRows = table.rowCount || 0;
|
|
505
1011
|
const batchSize = config.batchSize || 1000;
|
|
506
|
-
// Check if table is already synced for resumability
|
|
507
1012
|
const targetRowCount = await DataMigrator.getTargetRowCount(targetConnection, table.name);
|
|
508
1013
|
if (targetRowCount >= totalRows) {
|
|
509
1014
|
Logger.info(`Table ${table.name} already synced: ${targetRowCount}/${totalRows} rows, skipping`);
|
|
@@ -515,69 +1020,65 @@ export const DataMigrator = Object.freeze({
|
|
|
515
1020
|
else {
|
|
516
1021
|
Logger.info(`Processing ${totalRows} rows in batches of ${batchSize}`);
|
|
517
1022
|
}
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
try {
|
|
523
|
-
const chunk = await DataMigrator.readDataChunk(sourceConnection, table.name, offset, batchSize);
|
|
524
|
-
if (chunk.length === 0)
|
|
525
|
-
break;
|
|
526
|
-
// Transform data for D1 compatibility
|
|
527
|
-
const transformedChunk = await DataMigrator.transformData(chunk, table.name);
|
|
528
|
-
// Insert data into target
|
|
529
|
-
const insertedRows = await DataMigrator.insertData(targetConnection, table.name, transformedChunk);
|
|
530
|
-
if (insertedRows !== chunk.length) {
|
|
531
|
-
const verificationError = DataMigrator.createChunkVerificationError(table.name, offset, chunk.length, insertedRows);
|
|
532
|
-
throw ErrorFactory.createValidationError(`Chunk insert mismatch on ${table.name}`, verificationError);
|
|
533
|
-
}
|
|
534
|
-
rowsMigrated += insertedRows;
|
|
535
|
-
// Log progress for large tables
|
|
536
|
-
if (totalRows > 10000 && rowsMigrated % (batchSize * 10) === 0) {
|
|
537
|
-
const normalizedTotalRows = Math.max(totalRows, rowsMigrated);
|
|
538
|
-
const percentage = Math.round((rowsMigrated / normalizedTotalRows) * 100);
|
|
539
|
-
Logger.info(`Table ${table.name}: ${rowsMigrated}/${normalizedTotalRows} (${percentage}%)`);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
catch (error) {
|
|
543
|
-
const errorMsg = `Chunk processing failed at offset ${offset}: ${error}`;
|
|
544
|
-
Logger.error(errorMsg);
|
|
545
|
-
errors.push(errorMsg);
|
|
546
|
-
// Continue with next chunk instead of failing completely
|
|
547
|
-
continue;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
Logger.info(`Table ${table.name} completed: ${rowsMigrated} rows migrated`);
|
|
1023
|
+
rowsMigrated = await DataMigrator.processTableChunks(table, sourceConnection, targetConnection, totalRows, batchSize, targetRowCount, errors);
|
|
1024
|
+
const tableDurationMs = Date.now() - tableStartTime;
|
|
1025
|
+
Logger.info(`[DataMigrator] Table ${table.name} completed rows=${rowsMigrated}/${totalRows} duration=${formatDuration(tableDurationMs)} rate=${formatRowsPerSecond(rowsMigrated, tableDurationMs)}`);
|
|
1026
|
+
return { rowsMigrated, errors };
|
|
551
1027
|
}
|
|
552
1028
|
catch (error) {
|
|
553
|
-
const errorMsg = `
|
|
1029
|
+
const errorMsg = `Table migration failed for ${table.name}: ${error}`;
|
|
554
1030
|
Logger.error(errorMsg);
|
|
555
1031
|
errors.push(errorMsg);
|
|
1032
|
+
return { rowsMigrated, errors };
|
|
1033
|
+
}
|
|
1034
|
+
},
|
|
1035
|
+
async processTableChunks(table, sourceConnection, targetConnection, totalRows, batchSize, startOffset, errors) {
|
|
1036
|
+
let state = {
|
|
1037
|
+
rowsMigrated: 0,
|
|
1038
|
+
offset: startOffset,
|
|
1039
|
+
currentBatchSize: batchSize,
|
|
1040
|
+
retryBatchSize: batchSize,
|
|
1041
|
+
};
|
|
1042
|
+
while (state.offset < totalRows) {
|
|
1043
|
+
state.currentBatchSize = getTableChunkBatchSize(sourceConnection, state.retryBatchSize);
|
|
1044
|
+
const result = await processTableChunk(table, sourceConnection, targetConnection, state.offset, state.currentBatchSize, errors);
|
|
1045
|
+
state = applyTableChunkResult(state, result, batchSize, table, totalRows);
|
|
1046
|
+
if (!result.continueProcessing &&
|
|
1047
|
+
!(result.rowsMigrated === 0 && result.nextBatchSize < batchSize)) {
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
556
1050
|
}
|
|
557
|
-
return
|
|
1051
|
+
return state.rowsMigrated;
|
|
558
1052
|
},
|
|
559
1053
|
/**
|
|
560
1054
|
* Read data chunk from source database
|
|
561
1055
|
*/
|
|
562
1056
|
async readDataChunk(sourceConnection, tableName, offset, batchSize) {
|
|
563
|
-
Logger.debug(`Reading chunk from ${tableName}: offset ${offset}, size ${batchSize}`);
|
|
564
1057
|
if (!sourceConnection.adapter)
|
|
565
1058
|
return [];
|
|
566
1059
|
try {
|
|
567
1060
|
const selectSql = DataMigrator.buildSelectChunkSQL(sourceConnection.driver, tableName);
|
|
1061
|
+
const startTime = Date.now();
|
|
568
1062
|
const result = await sourceConnection.adapter.query(`${selectSql} LIMIT ${batchSize} OFFSET ${offset}`, []);
|
|
1063
|
+
const durationMs = Date.now() - startTime;
|
|
1064
|
+
if (sourceConnection.driver === 'mysql') {
|
|
1065
|
+
updateSourceBatchTuningAfterSuccess(sourceConnection, result.rows.length, durationMs, batchSize);
|
|
1066
|
+
}
|
|
569
1067
|
return result.rows || [];
|
|
570
1068
|
}
|
|
571
1069
|
catch (error) {
|
|
572
|
-
|
|
573
|
-
|
|
1070
|
+
const errorMessage = getErrorMessage(error);
|
|
1071
|
+
if (sourceConnection.driver === 'mysql' && isRetryableConnectionError(error)) {
|
|
1072
|
+
reduceSourceBatchSizeAfterTimeout(sourceConnection, batchSize);
|
|
1073
|
+
}
|
|
1074
|
+
Logger.warn(`Chunk read failed: ${errorMessage}`);
|
|
1075
|
+
throw error;
|
|
574
1076
|
}
|
|
575
1077
|
},
|
|
576
1078
|
/**
|
|
577
1079
|
* Transform data for D1 compatibility
|
|
578
1080
|
*/
|
|
579
|
-
async transformData(chunk,
|
|
580
|
-
Logger.debug(`Transforming ${chunk.length} rows for table ${tableName}`);
|
|
1081
|
+
async transformData(chunk, _tableName) {
|
|
581
1082
|
return chunk.map((row) => {
|
|
582
1083
|
const transformed = {};
|
|
583
1084
|
for (const [key, rawValue] of Object.entries(row)) {
|
|
@@ -612,27 +1113,36 @@ export const DataMigrator = Object.freeze({
|
|
|
612
1113
|
* Insert data into target database
|
|
613
1114
|
*/
|
|
614
1115
|
async insertData(targetConnection, tableName, data) {
|
|
615
|
-
Logger.debug(`Inserting ${data.length} rows into ${tableName}`);
|
|
616
1116
|
if (data.length === 0)
|
|
617
1117
|
return 0;
|
|
618
1118
|
if (!targetConnection.adapter) {
|
|
619
1119
|
throw ErrorFactory.createValidationError(`No target adapter configured for ${targetConnection.database}`);
|
|
620
1120
|
}
|
|
621
|
-
const
|
|
622
|
-
const
|
|
623
|
-
const
|
|
624
|
-
|
|
1121
|
+
const batchSettings = getInsertBatchSettings(targetConnection);
|
|
1122
|
+
const statements = createInsertStatements(targetConnection.type, batchSettings, tableName, data);
|
|
1123
|
+
const executableStatements = targetConnection.type === 'd1-remote'
|
|
1124
|
+
? createRemoteExecutionBatchesWithLimit(statements, batchSettings.maxExecutionSqlLength)
|
|
1125
|
+
: statements;
|
|
625
1126
|
let insertedRows = 0;
|
|
626
|
-
for (const
|
|
627
|
-
const values = keys.map((key) => row[key]);
|
|
1127
|
+
for (const statement of executableStatements) {
|
|
628
1128
|
try {
|
|
629
|
-
|
|
630
|
-
|
|
1129
|
+
const executionStartTime = Date.now();
|
|
1130
|
+
const result = await targetConnection.adapter.query(statement.sql, statement.parameters);
|
|
1131
|
+
const executionDurationMs = Date.now() - executionStartTime;
|
|
1132
|
+
const affectedRows = typeof result.rowCount === 'number' ? result.rowCount : statement.rowCount;
|
|
1133
|
+
insertedRows += affectedRows;
|
|
1134
|
+
adjustRemoteBatchTuning(targetConnection, affectedRows, executionDurationMs, statement.sql.length);
|
|
631
1135
|
}
|
|
632
1136
|
catch (error) {
|
|
1137
|
+
if (targetConnection.type === 'd1-remote' && data.length > 1 && statement.rowCount > 1) {
|
|
1138
|
+
reduceRemoteBatchTuningAfterFailure(targetConnection, statement.sql.length);
|
|
1139
|
+
const midpoint = Math.max(1, Math.floor(data.length / 2));
|
|
1140
|
+
const left = await DataMigrator.insertData(targetConnection, tableName, data.slice(0, midpoint));
|
|
1141
|
+
const right = await DataMigrator.insertData(targetConnection, tableName, data.slice(midpoint));
|
|
1142
|
+
return left + right;
|
|
1143
|
+
}
|
|
633
1144
|
throw ErrorFactory.createValidationError(`Insert failed for table ${tableName}`, {
|
|
634
|
-
|
|
635
|
-
row,
|
|
1145
|
+
rowCount: statement.rowCount,
|
|
636
1146
|
cause: error,
|
|
637
1147
|
});
|
|
638
1148
|
}
|
|
@@ -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;
|
|
@@ -578,8 +537,7 @@ export const MigrateToD1Command = BaseCommand.create({
|
|
|
578
537
|
execute: async (options) => {
|
|
579
538
|
try {
|
|
580
539
|
Logger.info('Starting D1 migration process...');
|
|
581
|
-
const { config, schemaOnly
|
|
582
|
-
logSourceConnectionDiagnostics(config.sourceDriver, config.sourceConnection, sourceConnectionOrigin, originalSourceConnection);
|
|
540
|
+
const { config, schemaOnly } = resolveMigrationConfig(options);
|
|
583
541
|
const configValidation = validateConfig(config);
|
|
584
542
|
if (!configValidation.valid) {
|
|
585
543
|
throw ErrorFactory.createValidationError(`Invalid migration configuration: ${configValidation.errors.join(', ')}`);
|
package/dist/types.d.ts
CHANGED
|
@@ -17,6 +17,10 @@ export interface MigrationConfig {
|
|
|
17
17
|
dryRun?: boolean;
|
|
18
18
|
interactive?: boolean;
|
|
19
19
|
migrationId?: string;
|
|
20
|
+
sourceSocketTimeoutMs?: number;
|
|
21
|
+
sourceWaitTimeoutSeconds?: number;
|
|
22
|
+
sourceNetReadTimeoutSeconds?: number;
|
|
23
|
+
sourceNetWriteTimeoutSeconds?: number;
|
|
20
24
|
}
|
|
21
25
|
export interface MigrationState {
|
|
22
26
|
id: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,YAAY,GAAG,QAAQ,GAAG,WAAW,CAAC;AAEnF,MAAM,MAAM,sBAAsB,GAAG,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,gBAAgB,EAAE,MAAM,CAAC;IACzB,sBAAsB,CAAC,EAAE,sBAAsB,CAAC;IAChD,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,EAAE,oBAAoB,CAAC;IACnC,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,IAAI,GAAG,WAAW,CAAC;IAC/B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zintrust/d1-migrator",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.5",
|
|
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": "^
|
|
44
|
+
"@zintrust/db-mysql": "^2.0.1",
|
|
45
45
|
"@zintrust/db-postgres": "^1.8.0",
|
|
46
46
|
"@zintrust/db-sqlite": "^1.8.0",
|
|
47
47
|
"@zintrust/db-sqlserver": "^1.8.0"
|