@rog0x/mcp-database-tools 1.0.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 +61 -0
- package/dist/index.js +190 -0
- package/package.json +37 -0
- package/src/index.ts +227 -0
- package/src/tools/migration-generator.ts +497 -0
- package/src/tools/query-builder.ts +469 -0
- package/src/tools/schema-analyzer.ts +370 -0
- package/src/tools/sql-explainer.ts +573 -0
- package/src/tools/sql-formatter.ts +233 -0
- package/tsconfig.json +14 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration Generator — compare two schemas (old vs new CREATE TABLE)
|
|
3
|
+
* and generate ALTER TABLE migration statements.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
interface ColumnDef {
|
|
7
|
+
name: string;
|
|
8
|
+
type: string;
|
|
9
|
+
nullable: boolean;
|
|
10
|
+
defaultValue: string | null;
|
|
11
|
+
isPrimaryKey: boolean;
|
|
12
|
+
isUnique: boolean;
|
|
13
|
+
autoIncrement: boolean;
|
|
14
|
+
fullDefinition: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface TableDef {
|
|
18
|
+
name: string;
|
|
19
|
+
columns: Map<string, ColumnDef>;
|
|
20
|
+
primaryKey: string[];
|
|
21
|
+
foreignKeys: FKDef[];
|
|
22
|
+
uniqueConstraints: string[][];
|
|
23
|
+
indexes: IdxDef[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FKDef {
|
|
27
|
+
constraintName: string | null;
|
|
28
|
+
columns: string[];
|
|
29
|
+
referencedTable: string;
|
|
30
|
+
referencedColumns: string[];
|
|
31
|
+
onDelete: string | null;
|
|
32
|
+
onUpdate: string | null;
|
|
33
|
+
raw: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface IdxDef {
|
|
37
|
+
name: string | null;
|
|
38
|
+
columns: string[];
|
|
39
|
+
unique: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface MigrationResult {
|
|
43
|
+
upStatements: string[];
|
|
44
|
+
downStatements: string[];
|
|
45
|
+
summary: string;
|
|
46
|
+
changes: ChangeDescription[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface ChangeDescription {
|
|
50
|
+
type: "add_table" | "drop_table" | "add_column" | "drop_column" | "modify_column" | "add_fk" | "drop_fk" | "add_index" | "drop_index" | "add_unique" | "drop_unique";
|
|
51
|
+
table: string;
|
|
52
|
+
detail: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function splitRespectingParens(text: string): string[] {
|
|
56
|
+
const parts: string[] = [];
|
|
57
|
+
let depth = 0;
|
|
58
|
+
let current = "";
|
|
59
|
+
for (const ch of text) {
|
|
60
|
+
if (ch === "(") depth++;
|
|
61
|
+
if (ch === ")") depth--;
|
|
62
|
+
if (ch === "," && depth === 0) {
|
|
63
|
+
parts.push(current.trim());
|
|
64
|
+
current = "";
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
current += ch;
|
|
68
|
+
}
|
|
69
|
+
if (current.trim()) parts.push(current.trim());
|
|
70
|
+
return parts;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function extractTableBody(stmt: string): string | null {
|
|
74
|
+
let depth = 0;
|
|
75
|
+
let start = -1;
|
|
76
|
+
let end = -1;
|
|
77
|
+
for (let i = 0; i < stmt.length; i++) {
|
|
78
|
+
if (stmt[i] === "(") {
|
|
79
|
+
if (depth === 0) start = i + 1;
|
|
80
|
+
depth++;
|
|
81
|
+
}
|
|
82
|
+
if (stmt[i] === ")") {
|
|
83
|
+
depth--;
|
|
84
|
+
if (depth === 0) {
|
|
85
|
+
end = i;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (start === -1 || end === -1) return null;
|
|
91
|
+
return stmt.substring(start, end).trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseColumnDef(def: string): ColumnDef | null {
|
|
95
|
+
const trimmed = def.trim();
|
|
96
|
+
if (/^\s*(PRIMARY KEY|FOREIGN KEY|UNIQUE|CHECK|CONSTRAINT|INDEX|KEY)\b/i.test(trimmed)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const parts = trimmed.split(/\s+/);
|
|
101
|
+
if (parts.length < 2) return null;
|
|
102
|
+
|
|
103
|
+
const name = parts[0].replace(/[`"[\]]/g, "");
|
|
104
|
+
const upper = trimmed.toUpperCase();
|
|
105
|
+
|
|
106
|
+
// Extract type
|
|
107
|
+
const afterName = trimmed.substring(parts[0].length).trim();
|
|
108
|
+
const typeMatch = afterName.match(
|
|
109
|
+
/^(\w+(?:\s*\([^)]*\))?(?:\s+(?:PRECISION|VARYING|WITHOUT|WITH|TIME|ZONE)(?:\s*\([^)]*\))?)*)/i
|
|
110
|
+
);
|
|
111
|
+
const type = typeMatch ? typeMatch[1].trim().toUpperCase() : parts[1].toUpperCase();
|
|
112
|
+
|
|
113
|
+
// Extract default
|
|
114
|
+
const defaultMatch = trimmed.match(/\bDEFAULT\s+('[^']*'|"[^"]*"|\S+)/i);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
name,
|
|
118
|
+
type,
|
|
119
|
+
nullable: !upper.includes("NOT NULL"),
|
|
120
|
+
defaultValue: defaultMatch ? defaultMatch[1] : null,
|
|
121
|
+
isPrimaryKey: upper.includes("PRIMARY KEY"),
|
|
122
|
+
isUnique: upper.includes("UNIQUE"),
|
|
123
|
+
autoIncrement:
|
|
124
|
+
upper.includes("AUTO_INCREMENT") ||
|
|
125
|
+
upper.includes("AUTOINCREMENT") ||
|
|
126
|
+
/\bSERIAL\b|\bBIGSERIAL\b|\bSMALLSERIAL\b/i.test(type),
|
|
127
|
+
fullDefinition: trimmed,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseSchema(sql: string): Map<string, TableDef> {
|
|
132
|
+
const tables = new Map<string, TableDef>();
|
|
133
|
+
const statements = sql.split(";").map((s) => s.trim()).filter(Boolean);
|
|
134
|
+
|
|
135
|
+
for (const stmt of statements) {
|
|
136
|
+
if (!/^\s*CREATE\s+TABLE\b/i.test(stmt)) continue;
|
|
137
|
+
|
|
138
|
+
const nameMatch = stmt.match(
|
|
139
|
+
/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`?(\w+)`?\.)?`?(\w+)`?/i
|
|
140
|
+
);
|
|
141
|
+
if (!nameMatch) continue;
|
|
142
|
+
const tableName = nameMatch[2].toLowerCase();
|
|
143
|
+
|
|
144
|
+
const body = extractTableBody(stmt);
|
|
145
|
+
if (!body) continue;
|
|
146
|
+
|
|
147
|
+
const definitions = splitRespectingParens(body);
|
|
148
|
+
const columns = new Map<string, ColumnDef>();
|
|
149
|
+
const primaryKey: string[] = [];
|
|
150
|
+
const foreignKeys: FKDef[] = [];
|
|
151
|
+
const uniqueConstraints: string[][] = [];
|
|
152
|
+
const indexes: IdxDef[] = [];
|
|
153
|
+
|
|
154
|
+
for (const def of definitions) {
|
|
155
|
+
const trimmed = def.trim();
|
|
156
|
+
const upper = trimmed.toUpperCase();
|
|
157
|
+
|
|
158
|
+
// PRIMARY KEY
|
|
159
|
+
if (/^\s*(?:CONSTRAINT\s+\w+\s+)?PRIMARY\s+KEY\s*\(/i.test(trimmed)) {
|
|
160
|
+
const m = trimmed.match(/PRIMARY\s+KEY\s*\(([^)]+)\)/i);
|
|
161
|
+
if (m) primaryKey.push(...m[1].split(",").map((c) => c.trim().replace(/[`"[\]]/g, "").toLowerCase()));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// FOREIGN KEY
|
|
166
|
+
if (/^\s*(?:CONSTRAINT\s+\w+\s+)?FOREIGN\s+KEY\s*\(/i.test(trimmed)) {
|
|
167
|
+
const cnMatch = trimmed.match(/CONSTRAINT\s+`?(\w+)`?/i);
|
|
168
|
+
const fkMatch = trimmed.match(
|
|
169
|
+
/FOREIGN\s+KEY\s*\(([^)]+)\)\s*REFERENCES\s+`?(\w+)`?\s*\(([^)]+)\)/i
|
|
170
|
+
);
|
|
171
|
+
if (fkMatch) {
|
|
172
|
+
const onDel = trimmed.match(/ON\s+DELETE\s+(\w+(?:\s+\w+)?)/i);
|
|
173
|
+
const onUpd = trimmed.match(/ON\s+UPDATE\s+(\w+(?:\s+\w+)?)/i);
|
|
174
|
+
foreignKeys.push({
|
|
175
|
+
constraintName: cnMatch ? cnMatch[1] : null,
|
|
176
|
+
columns: fkMatch[1].split(",").map((c) => c.trim().replace(/[`"[\]]/g, "").toLowerCase()),
|
|
177
|
+
referencedTable: fkMatch[2].toLowerCase(),
|
|
178
|
+
referencedColumns: fkMatch[3].split(",").map((c) => c.trim().replace(/[`"[\]]/g, "").toLowerCase()),
|
|
179
|
+
onDelete: onDel ? onDel[1].toUpperCase() : null,
|
|
180
|
+
onUpdate: onUpd ? onUpd[1].toUpperCase() : null,
|
|
181
|
+
raw: trimmed,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// UNIQUE
|
|
188
|
+
if (/^\s*(?:CONSTRAINT\s+\w+\s+)?UNIQUE\s*\(/i.test(trimmed)) {
|
|
189
|
+
const m = trimmed.match(/UNIQUE\s*\(([^)]+)\)/i);
|
|
190
|
+
if (m) uniqueConstraints.push(m[1].split(",").map((c) => c.trim().replace(/[`"[\]]/g, "").toLowerCase()));
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// INDEX / KEY
|
|
195
|
+
if (/^\s*(?:INDEX|KEY)\s+/i.test(trimmed)) {
|
|
196
|
+
const m = trimmed.match(/(?:INDEX|KEY)\s+`?(\w+)`?\s*\(([^)]+)\)/i);
|
|
197
|
+
if (m) {
|
|
198
|
+
indexes.push({
|
|
199
|
+
name: m[1],
|
|
200
|
+
columns: m[2].split(",").map((c) => c.trim().replace(/[`"[\]]/g, "").toLowerCase()),
|
|
201
|
+
unique: false,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// CHECK / CONSTRAINT CHECK
|
|
208
|
+
if (/^\s*(?:CONSTRAINT\s+\w+\s+)?CHECK\s*\(/i.test(trimmed)) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Column
|
|
213
|
+
const col = parseColumnDef(trimmed);
|
|
214
|
+
if (col) {
|
|
215
|
+
columns.set(col.name.toLowerCase(), col);
|
|
216
|
+
if (col.isPrimaryKey) primaryKey.push(col.name.toLowerCase());
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
tables.set(tableName, {
|
|
221
|
+
name: tableName,
|
|
222
|
+
columns,
|
|
223
|
+
primaryKey,
|
|
224
|
+
foreignKeys,
|
|
225
|
+
uniqueConstraints,
|
|
226
|
+
indexes,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return tables;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildColumnDefinition(col: ColumnDef): string {
|
|
234
|
+
let def = `${col.name} ${col.type}`;
|
|
235
|
+
if (!col.nullable) def += " NOT NULL";
|
|
236
|
+
if (col.defaultValue) def += ` DEFAULT ${col.defaultValue}`;
|
|
237
|
+
if (col.isUnique) def += " UNIQUE";
|
|
238
|
+
return def;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function migrationGenerator(
|
|
242
|
+
oldSchema: string,
|
|
243
|
+
newSchema: string,
|
|
244
|
+
dialect: string = "postgresql"
|
|
245
|
+
): MigrationResult {
|
|
246
|
+
const oldTables = parseSchema(oldSchema);
|
|
247
|
+
const newTables = parseSchema(newSchema);
|
|
248
|
+
|
|
249
|
+
const upStatements: string[] = [];
|
|
250
|
+
const downStatements: string[] = [];
|
|
251
|
+
const changes: ChangeDescription[] = [];
|
|
252
|
+
|
|
253
|
+
// Find new tables (exist in new but not old)
|
|
254
|
+
for (const [name, newTable] of newTables) {
|
|
255
|
+
if (!oldTables.has(name)) {
|
|
256
|
+
// Reconstruct CREATE TABLE
|
|
257
|
+
const colDefs = Array.from(newTable.columns.values()).map((c) => buildColumnDefinition(c));
|
|
258
|
+
if (newTable.primaryKey.length > 0) {
|
|
259
|
+
colDefs.push(`PRIMARY KEY (${newTable.primaryKey.join(", ")})`);
|
|
260
|
+
}
|
|
261
|
+
for (const fk of newTable.foreignKeys) {
|
|
262
|
+
let fkDef = `FOREIGN KEY (${fk.columns.join(", ")}) REFERENCES ${fk.referencedTable} (${fk.referencedColumns.join(", ")})`;
|
|
263
|
+
if (fk.onDelete) fkDef += ` ON DELETE ${fk.onDelete}`;
|
|
264
|
+
if (fk.onUpdate) fkDef += ` ON UPDATE ${fk.onUpdate}`;
|
|
265
|
+
colDefs.push(fkDef);
|
|
266
|
+
}
|
|
267
|
+
for (const uc of newTable.uniqueConstraints) {
|
|
268
|
+
colDefs.push(`UNIQUE (${uc.join(", ")})`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
upStatements.push(`CREATE TABLE ${name} (\n ${colDefs.join(",\n ")}\n);`);
|
|
272
|
+
downStatements.push(`DROP TABLE IF EXISTS ${name};`);
|
|
273
|
+
changes.push({ type: "add_table", table: name, detail: `Create new table "${name}"` });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Find dropped tables (exist in old but not new)
|
|
278
|
+
for (const [name, oldTable] of oldTables) {
|
|
279
|
+
if (!newTables.has(name)) {
|
|
280
|
+
const colDefs = Array.from(oldTable.columns.values()).map((c) => buildColumnDefinition(c));
|
|
281
|
+
if (oldTable.primaryKey.length > 0) {
|
|
282
|
+
colDefs.push(`PRIMARY KEY (${oldTable.primaryKey.join(", ")})`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
upStatements.push(`DROP TABLE IF EXISTS ${name};`);
|
|
286
|
+
downStatements.push(`CREATE TABLE ${name} (\n ${colDefs.join(",\n ")}\n);`);
|
|
287
|
+
changes.push({ type: "drop_table", table: name, detail: `Drop table "${name}"` });
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Compare tables that exist in both
|
|
292
|
+
for (const [name, newTable] of newTables) {
|
|
293
|
+
const oldTable = oldTables.get(name);
|
|
294
|
+
if (!oldTable) continue;
|
|
295
|
+
|
|
296
|
+
// New columns
|
|
297
|
+
for (const [colName, newCol] of newTable.columns) {
|
|
298
|
+
if (!oldTable.columns.has(colName)) {
|
|
299
|
+
const colDef = buildColumnDefinition(newCol);
|
|
300
|
+
upStatements.push(`ALTER TABLE ${name} ADD COLUMN ${colDef};`);
|
|
301
|
+
downStatements.push(`ALTER TABLE ${name} DROP COLUMN ${colName};`);
|
|
302
|
+
changes.push({
|
|
303
|
+
type: "add_column",
|
|
304
|
+
table: name,
|
|
305
|
+
detail: `Add column "${colName}" (${newCol.type})`,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Dropped columns
|
|
311
|
+
for (const [colName, oldCol] of oldTable.columns) {
|
|
312
|
+
if (!newTable.columns.has(colName)) {
|
|
313
|
+
upStatements.push(`ALTER TABLE ${name} DROP COLUMN ${colName};`);
|
|
314
|
+
const colDef = buildColumnDefinition(oldCol);
|
|
315
|
+
downStatements.push(`ALTER TABLE ${name} ADD COLUMN ${colDef};`);
|
|
316
|
+
changes.push({
|
|
317
|
+
type: "drop_column",
|
|
318
|
+
table: name,
|
|
319
|
+
detail: `Drop column "${colName}"`,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Modified columns
|
|
325
|
+
for (const [colName, newCol] of newTable.columns) {
|
|
326
|
+
const oldCol = oldTable.columns.get(colName);
|
|
327
|
+
if (!oldCol) continue;
|
|
328
|
+
|
|
329
|
+
const typeChanged = oldCol.type !== newCol.type;
|
|
330
|
+
const nullChanged = oldCol.nullable !== newCol.nullable;
|
|
331
|
+
const defaultChanged = oldCol.defaultValue !== newCol.defaultValue;
|
|
332
|
+
|
|
333
|
+
if (typeChanged || nullChanged || defaultChanged) {
|
|
334
|
+
const modifications: string[] = [];
|
|
335
|
+
|
|
336
|
+
if (typeChanged) {
|
|
337
|
+
if (dialect === "mysql") {
|
|
338
|
+
upStatements.push(
|
|
339
|
+
`ALTER TABLE ${name} MODIFY COLUMN ${buildColumnDefinition(newCol)};`
|
|
340
|
+
);
|
|
341
|
+
downStatements.push(
|
|
342
|
+
`ALTER TABLE ${name} MODIFY COLUMN ${buildColumnDefinition(oldCol)};`
|
|
343
|
+
);
|
|
344
|
+
} else {
|
|
345
|
+
upStatements.push(
|
|
346
|
+
`ALTER TABLE ${name} ALTER COLUMN ${colName} TYPE ${newCol.type};`
|
|
347
|
+
);
|
|
348
|
+
downStatements.push(
|
|
349
|
+
`ALTER TABLE ${name} ALTER COLUMN ${colName} TYPE ${oldCol.type};`
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
modifications.push(`type ${oldCol.type} -> ${newCol.type}`);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (nullChanged && !typeChanged) {
|
|
356
|
+
if (newCol.nullable) {
|
|
357
|
+
upStatements.push(
|
|
358
|
+
`ALTER TABLE ${name} ALTER COLUMN ${colName} DROP NOT NULL;`
|
|
359
|
+
);
|
|
360
|
+
downStatements.push(
|
|
361
|
+
`ALTER TABLE ${name} ALTER COLUMN ${colName} SET NOT NULL;`
|
|
362
|
+
);
|
|
363
|
+
} else {
|
|
364
|
+
upStatements.push(
|
|
365
|
+
`ALTER TABLE ${name} ALTER COLUMN ${colName} SET NOT NULL;`
|
|
366
|
+
);
|
|
367
|
+
downStatements.push(
|
|
368
|
+
`ALTER TABLE ${name} ALTER COLUMN ${colName} DROP NOT NULL;`
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
modifications.push(newCol.nullable ? "make nullable" : "make NOT NULL");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (defaultChanged && !typeChanged) {
|
|
375
|
+
if (newCol.defaultValue) {
|
|
376
|
+
upStatements.push(
|
|
377
|
+
`ALTER TABLE ${name} ALTER COLUMN ${colName} SET DEFAULT ${newCol.defaultValue};`
|
|
378
|
+
);
|
|
379
|
+
if (oldCol.defaultValue) {
|
|
380
|
+
downStatements.push(
|
|
381
|
+
`ALTER TABLE ${name} ALTER COLUMN ${colName} SET DEFAULT ${oldCol.defaultValue};`
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
downStatements.push(
|
|
385
|
+
`ALTER TABLE ${name} ALTER COLUMN ${colName} DROP DEFAULT;`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
upStatements.push(
|
|
390
|
+
`ALTER TABLE ${name} ALTER COLUMN ${colName} DROP DEFAULT;`
|
|
391
|
+
);
|
|
392
|
+
downStatements.push(
|
|
393
|
+
`ALTER TABLE ${name} ALTER COLUMN ${colName} SET DEFAULT ${oldCol.defaultValue};`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
modifications.push(`default ${oldCol.defaultValue || "none"} -> ${newCol.defaultValue || "none"}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (modifications.length > 0) {
|
|
400
|
+
changes.push({
|
|
401
|
+
type: "modify_column",
|
|
402
|
+
table: name,
|
|
403
|
+
detail: `Modify column "${colName}": ${modifications.join(", ")}`,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Foreign key changes
|
|
410
|
+
const oldFKKeys = oldTable.foreignKeys.map(
|
|
411
|
+
(fk) => `${fk.columns.join(",")}->${fk.referencedTable}(${fk.referencedColumns.join(",")})`
|
|
412
|
+
);
|
|
413
|
+
const newFKKeys = newTable.foreignKeys.map(
|
|
414
|
+
(fk) => `${fk.columns.join(",")}->${fk.referencedTable}(${fk.referencedColumns.join(",")})`
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
// New FKs
|
|
418
|
+
for (let i = 0; i < newTable.foreignKeys.length; i++) {
|
|
419
|
+
if (!oldFKKeys.includes(newFKKeys[i])) {
|
|
420
|
+
const fk = newTable.foreignKeys[i];
|
|
421
|
+
const cnName = fk.constraintName || `fk_${name}_${fk.columns.join("_")}`;
|
|
422
|
+
let fkDef = `ALTER TABLE ${name} ADD CONSTRAINT ${cnName} FOREIGN KEY (${fk.columns.join(", ")}) REFERENCES ${fk.referencedTable} (${fk.referencedColumns.join(", ")})`;
|
|
423
|
+
if (fk.onDelete) fkDef += ` ON DELETE ${fk.onDelete}`;
|
|
424
|
+
if (fk.onUpdate) fkDef += ` ON UPDATE ${fk.onUpdate}`;
|
|
425
|
+
upStatements.push(`${fkDef};`);
|
|
426
|
+
downStatements.push(`ALTER TABLE ${name} DROP CONSTRAINT ${cnName};`);
|
|
427
|
+
changes.push({
|
|
428
|
+
type: "add_fk",
|
|
429
|
+
table: name,
|
|
430
|
+
detail: `Add foreign key ${fk.columns.join(", ")} -> ${fk.referencedTable}(${fk.referencedColumns.join(", ")})`,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Dropped FKs
|
|
436
|
+
for (let i = 0; i < oldTable.foreignKeys.length; i++) {
|
|
437
|
+
if (!newFKKeys.includes(oldFKKeys[i])) {
|
|
438
|
+
const fk = oldTable.foreignKeys[i];
|
|
439
|
+
const cnName = fk.constraintName || `fk_${name}_${fk.columns.join("_")}`;
|
|
440
|
+
upStatements.push(`ALTER TABLE ${name} DROP CONSTRAINT ${cnName};`);
|
|
441
|
+
let fkDef = `ALTER TABLE ${name} ADD CONSTRAINT ${cnName} FOREIGN KEY (${fk.columns.join(", ")}) REFERENCES ${fk.referencedTable} (${fk.referencedColumns.join(", ")})`;
|
|
442
|
+
if (fk.onDelete) fkDef += ` ON DELETE ${fk.onDelete}`;
|
|
443
|
+
if (fk.onUpdate) fkDef += ` ON UPDATE ${fk.onUpdate}`;
|
|
444
|
+
downStatements.push(`${fkDef};`);
|
|
445
|
+
changes.push({
|
|
446
|
+
type: "drop_fk",
|
|
447
|
+
table: name,
|
|
448
|
+
detail: `Drop foreign key ${fk.columns.join(", ")} -> ${fk.referencedTable}(${fk.referencedColumns.join(", ")})`,
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Unique constraint changes
|
|
454
|
+
const oldUQKeys = oldTable.uniqueConstraints.map((uc) => uc.sort().join(","));
|
|
455
|
+
const newUQKeys = newTable.uniqueConstraints.map((uc) => uc.sort().join(","));
|
|
456
|
+
|
|
457
|
+
for (const uc of newTable.uniqueConstraints) {
|
|
458
|
+
const key = uc.sort().join(",");
|
|
459
|
+
if (!oldUQKeys.includes(key)) {
|
|
460
|
+
const cnName = `uq_${name}_${uc.join("_")}`;
|
|
461
|
+
upStatements.push(`ALTER TABLE ${name} ADD CONSTRAINT ${cnName} UNIQUE (${uc.join(", ")});`);
|
|
462
|
+
downStatements.push(`ALTER TABLE ${name} DROP CONSTRAINT ${cnName};`);
|
|
463
|
+
changes.push({
|
|
464
|
+
type: "add_unique",
|
|
465
|
+
table: name,
|
|
466
|
+
detail: `Add unique constraint on (${uc.join(", ")})`,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
for (const uc of oldTable.uniqueConstraints) {
|
|
472
|
+
const key = uc.sort().join(",");
|
|
473
|
+
if (!newUQKeys.includes(key)) {
|
|
474
|
+
const cnName = `uq_${name}_${uc.join("_")}`;
|
|
475
|
+
upStatements.push(`ALTER TABLE ${name} DROP CONSTRAINT ${cnName};`);
|
|
476
|
+
downStatements.push(`ALTER TABLE ${name} ADD CONSTRAINT ${cnName} UNIQUE (${uc.join(", ")});`);
|
|
477
|
+
changes.push({
|
|
478
|
+
type: "drop_unique",
|
|
479
|
+
table: name,
|
|
480
|
+
detail: `Drop unique constraint on (${uc.join(", ")})`,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Summary
|
|
487
|
+
const summary = changes.length > 0
|
|
488
|
+
? `Migration contains ${changes.length} change(s): ${changes.map((c) => c.detail).join("; ")}`
|
|
489
|
+
: "No differences detected between the two schemas.";
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
upStatements,
|
|
493
|
+
downStatements,
|
|
494
|
+
summary,
|
|
495
|
+
changes,
|
|
496
|
+
};
|
|
497
|
+
}
|