@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.
Files changed (35) hide show
  1. package/README.md +871 -0
  2. package/dist/cli/DataMigrator.d.ts +104 -0
  3. package/dist/cli/DataMigrator.d.ts.map +1 -0
  4. package/dist/cli/DataMigrator.js +431 -0
  5. package/dist/cli/MigrateToD1Command.d.ts +52 -0
  6. package/dist/cli/MigrateToD1Command.d.ts.map +1 -0
  7. package/dist/cli/MigrateToD1Command.js +600 -0
  8. package/dist/cli/ProgressTracker.d.ts +32 -0
  9. package/dist/cli/ProgressTracker.d.ts.map +1 -0
  10. package/dist/cli/ProgressTracker.js +95 -0
  11. package/dist/cli/SchemaAnalyzer.d.ts +130 -0
  12. package/dist/cli/SchemaAnalyzer.d.ts.map +1 -0
  13. package/dist/cli/SchemaAnalyzer.js +660 -0
  14. package/dist/index.d.ts +46 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +32 -0
  17. package/dist/schema/SchemaBuilder.d.ts +51 -0
  18. package/dist/schema/SchemaBuilder.d.ts.map +1 -0
  19. package/dist/schema/SchemaBuilder.js +165 -0
  20. package/dist/schema/TypeConverter.d.ts +35 -0
  21. package/dist/schema/TypeConverter.d.ts.map +1 -0
  22. package/dist/schema/TypeConverter.js +187 -0
  23. package/dist/schema/Validator.d.ts +74 -0
  24. package/dist/schema/Validator.d.ts.map +1 -0
  25. package/dist/schema/Validator.js +225 -0
  26. package/dist/types.d.ts +145 -0
  27. package/dist/types.d.ts.map +1 -0
  28. package/dist/types.js +5 -0
  29. package/dist/utils/CheckpointManager.d.ts +48 -0
  30. package/dist/utils/CheckpointManager.d.ts.map +1 -0
  31. package/dist/utils/CheckpointManager.js +191 -0
  32. package/dist/utils/DataValidator.d.ts +46 -0
  33. package/dist/utils/DataValidator.d.ts.map +1 -0
  34. package/dist/utils/DataValidator.js +139 -0
  35. 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
+ });