@zintrust/d1-migrator 0.4.0
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 +871 -0
- package/dist/cli/DataMigrator.d.ts +104 -0
- package/dist/cli/DataMigrator.d.ts.map +1 -0
- package/dist/cli/DataMigrator.js +431 -0
- package/dist/cli/MigrateToD1Command.d.ts +52 -0
- package/dist/cli/MigrateToD1Command.d.ts.map +1 -0
- package/dist/cli/MigrateToD1Command.js +600 -0
- package/dist/cli/ProgressTracker.d.ts +32 -0
- package/dist/cli/ProgressTracker.d.ts.map +1 -0
- package/dist/cli/ProgressTracker.js +95 -0
- package/dist/cli/SchemaAnalyzer.d.ts +130 -0
- package/dist/cli/SchemaAnalyzer.d.ts.map +1 -0
- package/dist/cli/SchemaAnalyzer.js +660 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/schema/SchemaBuilder.d.ts +51 -0
- package/dist/schema/SchemaBuilder.d.ts.map +1 -0
- package/dist/schema/SchemaBuilder.js +165 -0
- package/dist/schema/TypeConverter.d.ts +35 -0
- package/dist/schema/TypeConverter.d.ts.map +1 -0
- package/dist/schema/TypeConverter.js +187 -0
- package/dist/schema/Validator.d.ts +74 -0
- package/dist/schema/Validator.d.ts.map +1 -0
- package/dist/schema/Validator.js +225 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/utils/CheckpointManager.d.ts +48 -0
- package/dist/utils/CheckpointManager.d.ts.map +1 -0
- package/dist/utils/CheckpointManager.js +191 -0
- package/dist/utils/DataValidator.d.ts +46 -0
- package/dist/utils/DataValidator.d.ts.map +1 -0
- package/dist/utils/DataValidator.js +139 -0
- package/package.json +37 -0
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Analyzer
|
|
3
|
+
* Analyzes database schemas for migration compatibility
|
|
4
|
+
*/
|
|
5
|
+
import { ErrorFactory, Logger } from '@zintrust/core';
|
|
6
|
+
import { MySQLAdapter } from '@zintrust/db-mysql';
|
|
7
|
+
import { PostgreSQLAdapter } from '@zintrust/db-postgres';
|
|
8
|
+
import { SQLiteAdapter } from '@zintrust/db-sqlite';
|
|
9
|
+
import { SQLServerAdapter } from '@zintrust/db-sqlserver';
|
|
10
|
+
/**
|
|
11
|
+
* SchemaAnalyzer - Sealed namespace for schema analysis
|
|
12
|
+
* Provides database schema analysis and compatibility checking
|
|
13
|
+
*/
|
|
14
|
+
export const SchemaAnalyzer = Object.freeze({
|
|
15
|
+
/**
|
|
16
|
+
* Analyze source database schema
|
|
17
|
+
*/
|
|
18
|
+
async analyzeSchema(connection) {
|
|
19
|
+
Logger.info('Analyzing database schema...');
|
|
20
|
+
try {
|
|
21
|
+
// Connect to source database based on driver type
|
|
22
|
+
const tables = await SchemaAnalyzer.extractTables(connection);
|
|
23
|
+
const relationships = await SchemaAnalyzer.extractRelationships(connection, tables);
|
|
24
|
+
const constraints = await SchemaAnalyzer.extractConstraints(connection, tables);
|
|
25
|
+
const schema = {
|
|
26
|
+
tables,
|
|
27
|
+
relationships,
|
|
28
|
+
constraints,
|
|
29
|
+
};
|
|
30
|
+
Logger.info(`Found ${schema.tables.length} tables, ${schema.relationships.length} relationships`);
|
|
31
|
+
return schema;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
Logger.error('Failed to analyze database schema:', error);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
/**
|
|
39
|
+
* Extract tables from source database
|
|
40
|
+
*/
|
|
41
|
+
async extractTables(connection) {
|
|
42
|
+
Logger.info(`Extracting tables from ${connection.driver} database...`);
|
|
43
|
+
try {
|
|
44
|
+
// Create appropriate adapter based on driver
|
|
45
|
+
let adapter;
|
|
46
|
+
switch (connection.driver) {
|
|
47
|
+
case 'mysql':
|
|
48
|
+
adapter = MySQLAdapter.create({
|
|
49
|
+
driver: connection.driver,
|
|
50
|
+
connectionString: connection.connectionString,
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
case 'postgresql':
|
|
54
|
+
adapter = PostgreSQLAdapter.create({
|
|
55
|
+
driver: connection.driver,
|
|
56
|
+
});
|
|
57
|
+
break;
|
|
58
|
+
case 'sqlite':
|
|
59
|
+
adapter = SQLiteAdapter.create({
|
|
60
|
+
driver: connection.driver,
|
|
61
|
+
});
|
|
62
|
+
break;
|
|
63
|
+
case 'sqlserver':
|
|
64
|
+
adapter = SQLServerAdapter.create({
|
|
65
|
+
driver: connection.driver,
|
|
66
|
+
});
|
|
67
|
+
break;
|
|
68
|
+
default:
|
|
69
|
+
throw ErrorFactory.createValidationError(`Unsupported database driver: ${connection.driver}`);
|
|
70
|
+
}
|
|
71
|
+
// Connect to database
|
|
72
|
+
await adapter.connect();
|
|
73
|
+
// Get table list based on database type
|
|
74
|
+
const tables = await SchemaAnalyzer.getTableList(adapter, connection.driver);
|
|
75
|
+
// Extract detailed schema for each table in parallel for better performance
|
|
76
|
+
const tableSchemas = await Promise.all(tables.map((tableName) => SchemaAnalyzer.getTableSchema(adapter, tableName, connection.driver)));
|
|
77
|
+
await adapter.disconnect();
|
|
78
|
+
Logger.info(`Extracted ${tableSchemas.length} tables`);
|
|
79
|
+
return tableSchemas;
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
Logger.error('Failed to extract database tables:', error);
|
|
83
|
+
throw ErrorFactory.createTryCatchError('Schema extraction failed', error);
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
/**
|
|
87
|
+
* Extract relationships from source database
|
|
88
|
+
*/
|
|
89
|
+
async extractRelationships(_connection, _tables) {
|
|
90
|
+
Logger.info('Extracting table relationships...');
|
|
91
|
+
const relationships = [];
|
|
92
|
+
for (const table of _tables) {
|
|
93
|
+
for (const foreignKey of table.foreignKeys ?? []) {
|
|
94
|
+
relationships.push({
|
|
95
|
+
sourceTable: table.name,
|
|
96
|
+
sourceColumn: foreignKey.column,
|
|
97
|
+
targetTable: foreignKey.referencedTable,
|
|
98
|
+
targetColumn: foreignKey.referencedColumn,
|
|
99
|
+
type: 'one-to-many',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
Logger.info(`Extracted ${relationships.length} relationships`);
|
|
104
|
+
return relationships;
|
|
105
|
+
},
|
|
106
|
+
/**
|
|
107
|
+
* Extract constraints from source database
|
|
108
|
+
*/
|
|
109
|
+
async extractConstraints(_connection, _tables) {
|
|
110
|
+
Logger.info('Extracting table constraints...');
|
|
111
|
+
const constraints = [];
|
|
112
|
+
for (const table of _tables) {
|
|
113
|
+
if (table.primaryKeys.length > 0) {
|
|
114
|
+
constraints.push({
|
|
115
|
+
table: table.name,
|
|
116
|
+
type: 'primary_key',
|
|
117
|
+
columns: [...table.primaryKeys],
|
|
118
|
+
definition: `PRIMARY KEY (${table.primaryKeys.join(', ')})`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
for (const index of table.indexes ?? []) {
|
|
122
|
+
if (index.unique) {
|
|
123
|
+
constraints.push({
|
|
124
|
+
table: table.name,
|
|
125
|
+
type: 'unique',
|
|
126
|
+
columns: [...index.columns],
|
|
127
|
+
definition: `UNIQUE (${index.columns.join(', ')})`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const foreignKey of table.foreignKeys ?? []) {
|
|
132
|
+
constraints.push({
|
|
133
|
+
table: table.name,
|
|
134
|
+
type: 'foreign_key',
|
|
135
|
+
columns: [foreignKey.column],
|
|
136
|
+
definition: `FOREIGN KEY (${foreignKey.column}) REFERENCES ${foreignKey.referencedTable}(${foreignKey.referencedColumn})`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
Logger.info(`Extracted ${constraints.length} constraints`);
|
|
141
|
+
return constraints;
|
|
142
|
+
},
|
|
143
|
+
/**
|
|
144
|
+
* Check schema compatibility with D1
|
|
145
|
+
*/
|
|
146
|
+
checkD1Compatibility(schema) {
|
|
147
|
+
const issues = [];
|
|
148
|
+
const warnings = [];
|
|
149
|
+
// Check for unsupported features
|
|
150
|
+
schema.tables.forEach((table) => {
|
|
151
|
+
// Check for unsupported column types
|
|
152
|
+
table.columns.forEach((column) => {
|
|
153
|
+
if (!SchemaAnalyzer.isSupportedType(column.type)) {
|
|
154
|
+
issues.push(`Unsupported column type: ${column.type} in table ${table.name}`);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// Check for reserved keywords
|
|
158
|
+
if (!SchemaAnalyzer.isValidTableName(table.name)) {
|
|
159
|
+
issues.push(`Invalid table name: ${table.name}`);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
return {
|
|
163
|
+
compatible: issues.length === 0,
|
|
164
|
+
issues,
|
|
165
|
+
warnings,
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
/**
|
|
169
|
+
* Check if column type is supported by D1
|
|
170
|
+
*/
|
|
171
|
+
isSupportedType(type) {
|
|
172
|
+
const supportedTypes = [
|
|
173
|
+
'integer',
|
|
174
|
+
'text',
|
|
175
|
+
'real',
|
|
176
|
+
'numeric',
|
|
177
|
+
'blob',
|
|
178
|
+
'varchar',
|
|
179
|
+
'char',
|
|
180
|
+
'date',
|
|
181
|
+
'datetime',
|
|
182
|
+
'boolean',
|
|
183
|
+
];
|
|
184
|
+
return supportedTypes.includes(type.toLowerCase());
|
|
185
|
+
},
|
|
186
|
+
/**
|
|
187
|
+
* Check if table name is valid for D1
|
|
188
|
+
*/
|
|
189
|
+
isValidTableName(name) {
|
|
190
|
+
// Check for SQLite/D1 reserved keywords
|
|
191
|
+
const reserved = [
|
|
192
|
+
'select',
|
|
193
|
+
'insert',
|
|
194
|
+
'update',
|
|
195
|
+
'delete',
|
|
196
|
+
'create',
|
|
197
|
+
'drop',
|
|
198
|
+
'alter',
|
|
199
|
+
'index',
|
|
200
|
+
'table',
|
|
201
|
+
'database',
|
|
202
|
+
'primary',
|
|
203
|
+
'foreign',
|
|
204
|
+
'key',
|
|
205
|
+
'constraint',
|
|
206
|
+
'unique',
|
|
207
|
+
'not',
|
|
208
|
+
'null',
|
|
209
|
+
'default',
|
|
210
|
+
];
|
|
211
|
+
const normalizedName = name.toLowerCase();
|
|
212
|
+
return !reserved.includes(normalizedName) && /^\w*$/.test(name);
|
|
213
|
+
},
|
|
214
|
+
/**
|
|
215
|
+
* Generate schema analysis report
|
|
216
|
+
*/
|
|
217
|
+
generateReport(schema) {
|
|
218
|
+
let report = '# Database Schema Analysis Report\n\n';
|
|
219
|
+
report += `## Summary\n`;
|
|
220
|
+
report += `- Tables: ${schema.tables.length}\n`;
|
|
221
|
+
report += `- Relationships: ${schema.relationships.length}\n`;
|
|
222
|
+
report += `- Constraints: ${schema.constraints.length}\n\n`;
|
|
223
|
+
report += `## Tables\n\n`;
|
|
224
|
+
schema.tables.forEach((table) => {
|
|
225
|
+
report += `### ${table.name}\n`;
|
|
226
|
+
report += `- Columns: ${table.columns.length}\n`;
|
|
227
|
+
report += `- Primary Key: ${table.primaryKey || 'None'}\n\n`;
|
|
228
|
+
report += `#### Columns\n`;
|
|
229
|
+
table.columns.forEach((column) => {
|
|
230
|
+
report += `- ${column.name}: ${column.type}`;
|
|
231
|
+
if (column.nullable === false)
|
|
232
|
+
report += ' (NOT NULL)';
|
|
233
|
+
if (column.defaultValue !== undefined)
|
|
234
|
+
report += ` (DEFAULT: ${column.defaultValue})`;
|
|
235
|
+
report += '\n';
|
|
236
|
+
});
|
|
237
|
+
report += '\n';
|
|
238
|
+
});
|
|
239
|
+
return report;
|
|
240
|
+
},
|
|
241
|
+
/**
|
|
242
|
+
* Get table list from database based on driver type
|
|
243
|
+
*/
|
|
244
|
+
async getTableList(adapter, driver) {
|
|
245
|
+
switch (driver) {
|
|
246
|
+
case 'mysql': {
|
|
247
|
+
const result = await adapter.query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = DATABASE()', []);
|
|
248
|
+
return result.rows.map((row) => row['TABLE_NAME']);
|
|
249
|
+
}
|
|
250
|
+
case 'postgresql': {
|
|
251
|
+
const pgResult = await adapter.query('SELECT tablename FROM pg_tables WHERE schemaname = current_schema()', []);
|
|
252
|
+
return pgResult.rows.map((row) => row['tablename']);
|
|
253
|
+
}
|
|
254
|
+
case 'sqlite': {
|
|
255
|
+
const sqliteResult = await adapter.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", []);
|
|
256
|
+
return sqliteResult.rows.map((row) => row['name']);
|
|
257
|
+
}
|
|
258
|
+
case 'sqlserver': {
|
|
259
|
+
const sqlResult = await adapter.query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = BASE TABLE', []);
|
|
260
|
+
return sqlResult.rows.map((row) => row['TABLE_NAME']);
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
throw ErrorFactory.createValidationError(`Table listing not supported for driver: ${driver}`);
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
/**
|
|
267
|
+
* Get detailed schema for a specific table
|
|
268
|
+
*/
|
|
269
|
+
async getTableSchema(adapter, tableName, driver) {
|
|
270
|
+
try {
|
|
271
|
+
// Get column information
|
|
272
|
+
const columns = await SchemaAnalyzer.getTableColumns(adapter, tableName, driver);
|
|
273
|
+
// Get primary key information
|
|
274
|
+
const primaryKey = await SchemaAnalyzer.getPrimaryKey(adapter, tableName, driver);
|
|
275
|
+
// Get row count
|
|
276
|
+
const rowCountResult = await adapter.query(`SELECT COUNT(*) as count FROM ${tableName}`, []);
|
|
277
|
+
const rowCount = rowCountResult.rows[0]?.['count'] || 0;
|
|
278
|
+
// Get indexes
|
|
279
|
+
const indexes = await SchemaAnalyzer.getTableIndexes(adapter, tableName, driver);
|
|
280
|
+
// Get foreign keys
|
|
281
|
+
const foreignKeys = await SchemaAnalyzer.getForeignKeys(adapter, tableName, driver);
|
|
282
|
+
return {
|
|
283
|
+
name: tableName,
|
|
284
|
+
columns,
|
|
285
|
+
primaryKey: primaryKey || '',
|
|
286
|
+
primaryKeys: primaryKey ? [primaryKey] : [],
|
|
287
|
+
indexes,
|
|
288
|
+
foreignKeys,
|
|
289
|
+
rowCount,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
Logger.error(`Failed to get schema for table ${tableName}:`, error);
|
|
294
|
+
throw ErrorFactory.createTryCatchError(`Table schema extraction failed for ${tableName}`, error);
|
|
295
|
+
}
|
|
296
|
+
},
|
|
297
|
+
/**
|
|
298
|
+
* Get column information for a table
|
|
299
|
+
*/
|
|
300
|
+
async getTableColumns(adapter, tableName, driver) {
|
|
301
|
+
let query;
|
|
302
|
+
switch (driver) {
|
|
303
|
+
case 'mysql':
|
|
304
|
+
query = `
|
|
305
|
+
SELECT
|
|
306
|
+
COLUMN_NAME,
|
|
307
|
+
DATA_TYPE,
|
|
308
|
+
IS_NULLABLE,
|
|
309
|
+
COLUMN_DEFAULT,
|
|
310
|
+
EXTRA
|
|
311
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
312
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '${tableName}'
|
|
313
|
+
`;
|
|
314
|
+
break;
|
|
315
|
+
case 'postgresql':
|
|
316
|
+
query = `
|
|
317
|
+
SELECT
|
|
318
|
+
column_name,
|
|
319
|
+
data_type,
|
|
320
|
+
is_nullable,
|
|
321
|
+
column_default
|
|
322
|
+
FROM information_schema.columns
|
|
323
|
+
WHERE table_schema = current_schema() AND table_name = '${tableName}'
|
|
324
|
+
`;
|
|
325
|
+
break;
|
|
326
|
+
case 'sqlite':
|
|
327
|
+
query = `PRAGMA table_info(${tableName})`;
|
|
328
|
+
break;
|
|
329
|
+
case 'sqlserver':
|
|
330
|
+
query = `
|
|
331
|
+
SELECT
|
|
332
|
+
COLUMN_NAME,
|
|
333
|
+
DATA_TYPE,
|
|
334
|
+
IS_NULLABLE,
|
|
335
|
+
COLUMN_DEFAULT
|
|
336
|
+
FROM INFORMATION_SCHEMA.COLUMNS
|
|
337
|
+
WHERE TABLE_NAME = '${tableName}'
|
|
338
|
+
`;
|
|
339
|
+
break;
|
|
340
|
+
default:
|
|
341
|
+
throw ErrorFactory.createValidationError(`Column extraction not supported for driver: ${driver}`);
|
|
342
|
+
}
|
|
343
|
+
const result = await adapter.query(query, []);
|
|
344
|
+
return result.rows.map((row) => {
|
|
345
|
+
const column = {
|
|
346
|
+
name: (row['COLUMN_NAME'] || row['column_name'] || row['name']),
|
|
347
|
+
type: SchemaAnalyzer.normalizeDataType((row['DATA_TYPE'] || row['data_type'] || row['type']), driver),
|
|
348
|
+
nullable: (row['IS_NULLABLE'] || row['is_nullable'] || 'YES') === 'YES',
|
|
349
|
+
defaultValue: row['COLUMN_DEFAULT'] || row['column_default'],
|
|
350
|
+
autoIncrement: (row['EXTRA'] || row['extra'] || '').includes('auto_increment'),
|
|
351
|
+
};
|
|
352
|
+
// Clean up undefined values
|
|
353
|
+
column.defaultValue ??= undefined;
|
|
354
|
+
return column;
|
|
355
|
+
});
|
|
356
|
+
},
|
|
357
|
+
/**
|
|
358
|
+
* Get primary key for a table
|
|
359
|
+
*/
|
|
360
|
+
async getPrimaryKey(adapter, tableName, driver) {
|
|
361
|
+
let query;
|
|
362
|
+
switch (driver) {
|
|
363
|
+
case 'mysql':
|
|
364
|
+
query = `
|
|
365
|
+
SELECT COLUMN_NAME
|
|
366
|
+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
|
367
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '${tableName}' AND CONSTRAINT_NAME = 'PRIMARY'
|
|
368
|
+
`;
|
|
369
|
+
break;
|
|
370
|
+
case 'postgresql':
|
|
371
|
+
query = `
|
|
372
|
+
SELECT column_name
|
|
373
|
+
FROM information_schema.table_constraints tc
|
|
374
|
+
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
|
|
375
|
+
WHERE tc.table_schema = current_schema() AND tc.table_name = '${tableName}' AND tc.constraint_type = 'PRIMARY KEY'
|
|
376
|
+
`;
|
|
377
|
+
break;
|
|
378
|
+
case 'sqlite':
|
|
379
|
+
query = `PRAGMA table_info(${tableName})`;
|
|
380
|
+
break;
|
|
381
|
+
case 'sqlserver':
|
|
382
|
+
query = `
|
|
383
|
+
SELECT COLUMN_NAME
|
|
384
|
+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
|
385
|
+
WHERE TABLE_NAME = '${tableName}' AND CONSTRAINT_NAME = 'PRIMARY'
|
|
386
|
+
`;
|
|
387
|
+
break;
|
|
388
|
+
default:
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const result = await adapter.query(query, []);
|
|
393
|
+
return result.rows.length > 0
|
|
394
|
+
? result.rows[0]['COLUMN_NAME'] ||
|
|
395
|
+
result.rows[0]['column_name'] ||
|
|
396
|
+
result.rows[0]['name']
|
|
397
|
+
: null;
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
Logger.warn(`Could not determine primary key for ${tableName}:`, error);
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
/**
|
|
405
|
+
* Normalize data type from different database systems to D1-compatible types
|
|
406
|
+
*/
|
|
407
|
+
normalizeDataType(dataType, _driver) {
|
|
408
|
+
const type = (dataType || '').toLowerCase();
|
|
409
|
+
// Convert various data type formats to standard D1 types
|
|
410
|
+
const typeMap = {
|
|
411
|
+
// MySQL types
|
|
412
|
+
int: 'integer',
|
|
413
|
+
varchar: 'varchar',
|
|
414
|
+
text: 'text',
|
|
415
|
+
datetime: 'datetime',
|
|
416
|
+
timestamp: 'datetime',
|
|
417
|
+
decimal: 'real',
|
|
418
|
+
double: 'real',
|
|
419
|
+
float: 'real',
|
|
420
|
+
boolean: 'boolean',
|
|
421
|
+
tinyint: 'boolean',
|
|
422
|
+
date: 'date',
|
|
423
|
+
// PostgreSQL types
|
|
424
|
+
'character varying': 'varchar',
|
|
425
|
+
'timestamp without time zone': 'datetime',
|
|
426
|
+
'timestamp with time zone': 'datetime',
|
|
427
|
+
numeric: 'real',
|
|
428
|
+
// SQLite types
|
|
429
|
+
blob: 'blob',
|
|
430
|
+
};
|
|
431
|
+
// Handle type with precision/length
|
|
432
|
+
const normalizedType = type.split('(')[0].trim();
|
|
433
|
+
return typeMap[normalizedType] || 'text';
|
|
434
|
+
},
|
|
435
|
+
/**
|
|
436
|
+
* Get indexes for a table
|
|
437
|
+
*/
|
|
438
|
+
async getTableIndexes(adapter, tableName, driver) {
|
|
439
|
+
const query = SchemaAnalyzer.buildIndexQuery(tableName, driver);
|
|
440
|
+
if (!query) {
|
|
441
|
+
return [];
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
const result = await adapter.query(query, []);
|
|
445
|
+
return SchemaAnalyzer.processIndexResults(result, driver);
|
|
446
|
+
}
|
|
447
|
+
catch (error) {
|
|
448
|
+
Logger.warn(`Could not determine indexes for ${tableName}:`, error);
|
|
449
|
+
return [];
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
/**
|
|
453
|
+
* Build index query for specific driver
|
|
454
|
+
*/
|
|
455
|
+
buildIndexQuery(tableName, driver) {
|
|
456
|
+
switch (driver) {
|
|
457
|
+
case 'mysql':
|
|
458
|
+
return `
|
|
459
|
+
SELECT
|
|
460
|
+
INDEX_NAME,
|
|
461
|
+
COLUMN_NAME,
|
|
462
|
+
NON_UNIQUE
|
|
463
|
+
FROM INFORMATION_SCHEMA.STATISTICS
|
|
464
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '${tableName}'
|
|
465
|
+
ORDER BY INDEX_NAME, SEQ_IN_INDEX
|
|
466
|
+
`;
|
|
467
|
+
case 'postgresql':
|
|
468
|
+
return `
|
|
469
|
+
SELECT
|
|
470
|
+
i.indexname,
|
|
471
|
+
a.attname,
|
|
472
|
+
i.indisunique
|
|
473
|
+
FROM pg_indexes i
|
|
474
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid
|
|
475
|
+
WHERE i.schemaname = current_schema() AND i.tablename = '${tableName}'
|
|
476
|
+
`;
|
|
477
|
+
case 'sqlite':
|
|
478
|
+
return `PRAGMA index_list(${tableName})`;
|
|
479
|
+
case 'sqlserver':
|
|
480
|
+
return `
|
|
481
|
+
SELECT
|
|
482
|
+
i.name AS INDEX_NAME,
|
|
483
|
+
c.name AS COLUMN_NAME,
|
|
484
|
+
i.is_unique
|
|
485
|
+
FROM sys.indexes i
|
|
486
|
+
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
|
487
|
+
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
|
488
|
+
JOIN sys.tables t ON i.object_id = t.object_id
|
|
489
|
+
WHERE t.name = '${tableName}'
|
|
490
|
+
`;
|
|
491
|
+
default:
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
/**
|
|
496
|
+
* Process index results into IndexSchema format
|
|
497
|
+
*/
|
|
498
|
+
processIndexResults(result, driver) {
|
|
499
|
+
const indexMap = new Map();
|
|
500
|
+
result.rows.forEach((row) => {
|
|
501
|
+
const indexName = (row['INDEX_NAME'] || row['indexname'] || row['name']);
|
|
502
|
+
const columnName = (row['COLUMN_NAME'] || row['attname'] || row['column_name']);
|
|
503
|
+
const isUnique = SchemaAnalyzer.isIndexUnique(row, driver);
|
|
504
|
+
if (!indexMap.has(indexName)) {
|
|
505
|
+
const newIndex = {
|
|
506
|
+
columns: [],
|
|
507
|
+
unique: isUnique,
|
|
508
|
+
primary: indexName === 'PRIMARY',
|
|
509
|
+
};
|
|
510
|
+
indexMap.set(indexName, newIndex);
|
|
511
|
+
}
|
|
512
|
+
const index = indexMap.get(indexName);
|
|
513
|
+
if (index && columnName && !index.columns.includes(columnName)) {
|
|
514
|
+
index.columns.push(columnName);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
return Array.from(indexMap.entries())
|
|
518
|
+
.filter(([name]) => name && name !== 'PRIMARY')
|
|
519
|
+
.map(([name, data]) => ({
|
|
520
|
+
name,
|
|
521
|
+
columns: data.columns,
|
|
522
|
+
unique: data.unique,
|
|
523
|
+
primary: data.primary,
|
|
524
|
+
}));
|
|
525
|
+
},
|
|
526
|
+
/**
|
|
527
|
+
* Check if index is unique based on driver-specific data
|
|
528
|
+
*/
|
|
529
|
+
isIndexUnique(row, driver) {
|
|
530
|
+
switch (driver) {
|
|
531
|
+
case 'mysql':
|
|
532
|
+
return row['NON_UNIQUE'] === 0;
|
|
533
|
+
case 'postgresql':
|
|
534
|
+
return row['indisunique'] === true;
|
|
535
|
+
case 'sqlserver':
|
|
536
|
+
return row['is_unique'] === true;
|
|
537
|
+
default:
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
/**
|
|
542
|
+
* Get foreign keys for a table
|
|
543
|
+
*/
|
|
544
|
+
async getForeignKeys(adapter, tableName, driver) {
|
|
545
|
+
const query = SchemaAnalyzer.buildForeignKeyQuery(tableName, driver);
|
|
546
|
+
if (!query) {
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
try {
|
|
550
|
+
const result = await adapter.query(query, []);
|
|
551
|
+
return result.rows.map((row) => SchemaAnalyzer.processForeignKeyRow(row, tableName));
|
|
552
|
+
}
|
|
553
|
+
catch (error) {
|
|
554
|
+
Logger.warn(`Could not determine foreign keys for ${tableName}:`, error);
|
|
555
|
+
return [];
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
/**
|
|
559
|
+
* Build foreign key query for specific driver
|
|
560
|
+
*/
|
|
561
|
+
buildForeignKeyQuery(tableName, driver) {
|
|
562
|
+
switch (driver) {
|
|
563
|
+
case 'mysql':
|
|
564
|
+
return `
|
|
565
|
+
SELECT
|
|
566
|
+
kcu.CONSTRAINT_NAME AS CONSTRAINT_NAME,
|
|
567
|
+
kcu.COLUMN_NAME AS COLUMN_NAME,
|
|
568
|
+
kcu.REFERENCED_TABLE_NAME AS REFERENCED_TABLE_NAME,
|
|
569
|
+
kcu.REFERENCED_COLUMN_NAME AS REFERENCED_COLUMN_NAME,
|
|
570
|
+
rc.DELETE_RULE AS DELETE_RULE,
|
|
571
|
+
rc.UPDATE_RULE AS UPDATE_RULE
|
|
572
|
+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
|
573
|
+
JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
|
|
574
|
+
ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
|
575
|
+
AND kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
|
|
576
|
+
WHERE kcu.TABLE_SCHEMA = DATABASE()
|
|
577
|
+
AND kcu.TABLE_NAME = '${tableName}'
|
|
578
|
+
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
|
579
|
+
`;
|
|
580
|
+
case 'postgresql':
|
|
581
|
+
return `
|
|
582
|
+
SELECT
|
|
583
|
+
tc.constraint_name,
|
|
584
|
+
kcu.column_name,
|
|
585
|
+
ccu.table_name AS referenced_table_name,
|
|
586
|
+
ccu.column_name AS referenced_column_name,
|
|
587
|
+
rc.delete_rule,
|
|
588
|
+
rc.update_rule
|
|
589
|
+
FROM information_schema.table_constraints AS tc
|
|
590
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
591
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
592
|
+
AND tc.table_schema = kcu.table_schema
|
|
593
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
594
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
595
|
+
AND ccu.table_schema = tc.table_schema
|
|
596
|
+
LEFT JOIN information_schema.referential_constraints rc
|
|
597
|
+
ON tc.constraint_name = rc.constraint_name
|
|
598
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
599
|
+
AND tc.table_name = '${tableName}'
|
|
600
|
+
AND tc.table_schema = current_schema()
|
|
601
|
+
`;
|
|
602
|
+
case 'sqlite':
|
|
603
|
+
return `PRAGMA foreign_key_list(${tableName})`;
|
|
604
|
+
case 'sqlserver':
|
|
605
|
+
return `
|
|
606
|
+
SELECT
|
|
607
|
+
f.name AS CONSTRAINT_NAME,
|
|
608
|
+
COL_NAME(fc.parent_column_id) AS COLUMN_NAME,
|
|
609
|
+
OBJECT_NAME(f.referenced_object_id) AS REFERENCED_TABLE_NAME,
|
|
610
|
+
COL_NAME(fc.referenced_column_id) AS REFERENCED_COLUMN_NAME,
|
|
611
|
+
f.delete_referential_action_desc AS DELETE_RULE,
|
|
612
|
+
f.update_referential_action_desc AS UPDATE_RULE
|
|
613
|
+
FROM sys.foreign_keys AS f
|
|
614
|
+
JOIN sys.foreign_key_columns AS fc ON f.object_id = fc.parent_object_id
|
|
615
|
+
WHERE OBJECT_NAME(f.parent_object_id) = '${tableName}'
|
|
616
|
+
`;
|
|
617
|
+
default:
|
|
618
|
+
return null;
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
/**
|
|
622
|
+
* Process foreign key row into ForeignKeySchema format
|
|
623
|
+
*/
|
|
624
|
+
processForeignKeyRow(row, tableName) {
|
|
625
|
+
const constraintName = (row['CONSTRAINT_NAME'] ||
|
|
626
|
+
row['constraint_name'] ||
|
|
627
|
+
`fk_${tableName}_${row['COLUMN_NAME']}`);
|
|
628
|
+
const columnName = (row['COLUMN_NAME'] || row['column_name'] || row['from']);
|
|
629
|
+
const referencedTable = (row['REFERENCED_TABLE_NAME'] ||
|
|
630
|
+
row['referenced_table_name'] ||
|
|
631
|
+
row['table']);
|
|
632
|
+
const referencedColumn = (row['REFERENCED_COLUMN_NAME'] ||
|
|
633
|
+
row['referenced_column_name'] ||
|
|
634
|
+
row['to']);
|
|
635
|
+
const deleteRule = (row['DELETE_RULE'] || row['delete_rule']);
|
|
636
|
+
const updateRule = (row['UPDATE_RULE'] || row['update_rule']);
|
|
637
|
+
const onDelete = SchemaAnalyzer.mapReferentialAction(deleteRule);
|
|
638
|
+
const onUpdate = SchemaAnalyzer.mapReferentialAction(updateRule);
|
|
639
|
+
return {
|
|
640
|
+
name: constraintName,
|
|
641
|
+
column: columnName,
|
|
642
|
+
referencedTable,
|
|
643
|
+
referencedColumn,
|
|
644
|
+
onDelete,
|
|
645
|
+
onUpdate,
|
|
646
|
+
};
|
|
647
|
+
},
|
|
648
|
+
/**
|
|
649
|
+
* Map referential action string to enum value
|
|
650
|
+
*/
|
|
651
|
+
mapReferentialAction(action) {
|
|
652
|
+
if (action === 'CASCADE') {
|
|
653
|
+
return 'CASCADE';
|
|
654
|
+
}
|
|
655
|
+
if (action === 'SET NULL') {
|
|
656
|
+
return 'SET NULL';
|
|
657
|
+
}
|
|
658
|
+
return 'RESTRICT';
|
|
659
|
+
},
|
|
660
|
+
});
|