@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,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Analyzer — parse CREATE TABLE statements to extract
|
|
3
|
+
* tables, columns, types, constraints, foreign keys, indexes.
|
|
4
|
+
* Generate ER diagrams in Mermaid format.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
interface ColumnInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
type: string;
|
|
10
|
+
nullable: boolean;
|
|
11
|
+
defaultValue: string | null;
|
|
12
|
+
isPrimaryKey: boolean;
|
|
13
|
+
isUnique: boolean;
|
|
14
|
+
autoIncrement: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ForeignKeyInfo {
|
|
18
|
+
columns: string[];
|
|
19
|
+
referencedTable: string;
|
|
20
|
+
referencedColumns: string[];
|
|
21
|
+
onDelete: string | null;
|
|
22
|
+
onUpdate: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface IndexInfo {
|
|
26
|
+
name: string | null;
|
|
27
|
+
columns: string[];
|
|
28
|
+
unique: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TableInfo {
|
|
32
|
+
name: string;
|
|
33
|
+
columns: ColumnInfo[];
|
|
34
|
+
primaryKey: string[];
|
|
35
|
+
foreignKeys: ForeignKeyInfo[];
|
|
36
|
+
indexes: IndexInfo[];
|
|
37
|
+
uniqueConstraints: string[][];
|
|
38
|
+
checkConstraints: string[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface SchemaAnalysis {
|
|
42
|
+
tables: TableInfo[];
|
|
43
|
+
relationships: {
|
|
44
|
+
from: string;
|
|
45
|
+
fromColumns: string[];
|
|
46
|
+
to: string;
|
|
47
|
+
toColumns: string[];
|
|
48
|
+
type: string;
|
|
49
|
+
}[];
|
|
50
|
+
mermaidDiagram: string;
|
|
51
|
+
summary: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function splitRespectingParens(text: string): string[] {
|
|
55
|
+
const parts: string[] = [];
|
|
56
|
+
let depth = 0;
|
|
57
|
+
let current = "";
|
|
58
|
+
for (const ch of text) {
|
|
59
|
+
if (ch === "(") depth++;
|
|
60
|
+
if (ch === ")") depth--;
|
|
61
|
+
if (ch === "," && depth === 0) {
|
|
62
|
+
parts.push(current.trim());
|
|
63
|
+
current = "";
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
current += ch;
|
|
67
|
+
}
|
|
68
|
+
if (current.trim()) parts.push(current.trim());
|
|
69
|
+
return parts;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function extractTableBody(createStmt: string): string | null {
|
|
73
|
+
// Find the outermost parentheses
|
|
74
|
+
let depth = 0;
|
|
75
|
+
let start = -1;
|
|
76
|
+
let end = -1;
|
|
77
|
+
for (let i = 0; i < createStmt.length; i++) {
|
|
78
|
+
if (createStmt[i] === "(") {
|
|
79
|
+
if (depth === 0) start = i + 1;
|
|
80
|
+
depth++;
|
|
81
|
+
}
|
|
82
|
+
if (createStmt[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 createStmt.substring(start, end).trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseColumn(def: string): ColumnInfo | null {
|
|
95
|
+
const trimmed = def.trim();
|
|
96
|
+
// Skip table-level constraints
|
|
97
|
+
if (/^\s*(PRIMARY KEY|FOREIGN KEY|UNIQUE|CHECK|CONSTRAINT|INDEX|KEY)\b/i.test(trimmed)) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const parts = trimmed.split(/\s+/);
|
|
102
|
+
if (parts.length < 2) return null;
|
|
103
|
+
|
|
104
|
+
const name = parts[0].replace(/[`"[\]]/g, "");
|
|
105
|
+
const upper = trimmed.toUpperCase();
|
|
106
|
+
|
|
107
|
+
// Type can be multi-word, e.g. "DOUBLE PRECISION", "CHARACTER VARYING(255)"
|
|
108
|
+
// Find everything after the name up to the first constraint keyword
|
|
109
|
+
const afterName = trimmed.substring(parts[0].length).trim();
|
|
110
|
+
const typeMatch = afterName.match(
|
|
111
|
+
/^(\w+(?:\s*\([^)]*\))?(?:\s+(?:PRECISION|VARYING|WITHOUT|WITH|TIME|ZONE)(?:\s*\([^)]*\))?)*)/i
|
|
112
|
+
);
|
|
113
|
+
const type = typeMatch ? typeMatch[1].trim() : parts[1];
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
type,
|
|
118
|
+
nullable: !upper.includes("NOT NULL"),
|
|
119
|
+
defaultValue: extractDefault(trimmed),
|
|
120
|
+
isPrimaryKey: upper.includes("PRIMARY KEY"),
|
|
121
|
+
isUnique: upper.includes("UNIQUE"),
|
|
122
|
+
autoIncrement:
|
|
123
|
+
upper.includes("AUTO_INCREMENT") ||
|
|
124
|
+
upper.includes("AUTOINCREMENT") ||
|
|
125
|
+
/\bSERIAL\b|\bBIGSERIAL\b|\bSMALLSERIAL\b/i.test(type),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function extractDefault(colDef: string): string | null {
|
|
130
|
+
const match = colDef.match(/\bDEFAULT\s+('[^']*'|"[^"]*"|\S+)/i);
|
|
131
|
+
return match ? match[1] : null;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseTable(createStmt: string): TableInfo | null {
|
|
135
|
+
const nameMatch = createStmt.match(
|
|
136
|
+
/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:`?(\w+)`?\.)?`?(\w+)`?/i
|
|
137
|
+
);
|
|
138
|
+
if (!nameMatch) return null;
|
|
139
|
+
const tableName = nameMatch[2];
|
|
140
|
+
|
|
141
|
+
const body = extractTableBody(createStmt);
|
|
142
|
+
if (!body) return null;
|
|
143
|
+
|
|
144
|
+
const definitions = splitRespectingParens(body);
|
|
145
|
+
|
|
146
|
+
const columns: ColumnInfo[] = [];
|
|
147
|
+
const primaryKey: string[] = [];
|
|
148
|
+
const foreignKeys: ForeignKeyInfo[] = [];
|
|
149
|
+
const indexes: IndexInfo[] = [];
|
|
150
|
+
const uniqueConstraints: string[][] = [];
|
|
151
|
+
const checkConstraints: string[] = [];
|
|
152
|
+
|
|
153
|
+
for (const def of definitions) {
|
|
154
|
+
const trimmed = def.trim();
|
|
155
|
+
const upper = trimmed.toUpperCase();
|
|
156
|
+
|
|
157
|
+
// Table-level PRIMARY KEY
|
|
158
|
+
if (/^\s*(?:CONSTRAINT\s+\w+\s+)?PRIMARY\s+KEY\s*\(/i.test(trimmed)) {
|
|
159
|
+
const colsMatch = trimmed.match(/PRIMARY\s+KEY\s*\(([^)]+)\)/i);
|
|
160
|
+
if (colsMatch) {
|
|
161
|
+
const cols = colsMatch[1].split(",").map((c) => c.trim().replace(/[`"[\]]/g, ""));
|
|
162
|
+
primaryKey.push(...cols);
|
|
163
|
+
}
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// FOREIGN KEY
|
|
168
|
+
if (/^\s*(?:CONSTRAINT\s+\w+\s+)?FOREIGN\s+KEY\s*\(/i.test(trimmed)) {
|
|
169
|
+
const fkMatch = trimmed.match(
|
|
170
|
+
/FOREIGN\s+KEY\s*\(([^)]+)\)\s*REFERENCES\s+`?(\w+)`?\s*\(([^)]+)\)/i
|
|
171
|
+
);
|
|
172
|
+
if (fkMatch) {
|
|
173
|
+
const fkCols = fkMatch[1].split(",").map((c) => c.trim().replace(/[`"[\]]/g, ""));
|
|
174
|
+
const refTable = fkMatch[2];
|
|
175
|
+
const refCols = fkMatch[3].split(",").map((c) => c.trim().replace(/[`"[\]]/g, ""));
|
|
176
|
+
const onDeleteMatch = trimmed.match(/ON\s+DELETE\s+(\w+(?:\s+\w+)?)/i);
|
|
177
|
+
const onUpdateMatch = trimmed.match(/ON\s+UPDATE\s+(\w+(?:\s+\w+)?)/i);
|
|
178
|
+
foreignKeys.push({
|
|
179
|
+
columns: fkCols,
|
|
180
|
+
referencedTable: refTable,
|
|
181
|
+
referencedColumns: refCols,
|
|
182
|
+
onDelete: onDeleteMatch ? onDeleteMatch[1] : null,
|
|
183
|
+
onUpdate: onUpdateMatch ? onUpdateMatch[1] : null,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// UNIQUE constraint
|
|
190
|
+
if (/^\s*(?:CONSTRAINT\s+\w+\s+)?UNIQUE\s*\(/i.test(trimmed)) {
|
|
191
|
+
const uqMatch = trimmed.match(/UNIQUE\s*\(([^)]+)\)/i);
|
|
192
|
+
if (uqMatch) {
|
|
193
|
+
const cols = uqMatch[1].split(",").map((c) => c.trim().replace(/[`"[\]]/g, ""));
|
|
194
|
+
uniqueConstraints.push(cols);
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// CHECK constraint
|
|
200
|
+
if (/^\s*(?:CONSTRAINT\s+\w+\s+)?CHECK\s*\(/i.test(trimmed)) {
|
|
201
|
+
checkConstraints.push(trimmed);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// INDEX / KEY (MySQL syntax in CREATE TABLE)
|
|
206
|
+
if (/^\s*(?:INDEX|KEY)\s+/i.test(trimmed)) {
|
|
207
|
+
const idxMatch = trimmed.match(/(?:INDEX|KEY)\s+`?(\w+)`?\s*\(([^)]+)\)/i);
|
|
208
|
+
if (idxMatch) {
|
|
209
|
+
indexes.push({
|
|
210
|
+
name: idxMatch[1],
|
|
211
|
+
columns: idxMatch[2].split(",").map((c) => c.trim().replace(/[`"[\]]/g, "")),
|
|
212
|
+
unique: false,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Column definition
|
|
219
|
+
const col = parseColumn(trimmed);
|
|
220
|
+
if (col) {
|
|
221
|
+
columns.push(col);
|
|
222
|
+
if (col.isPrimaryKey) {
|
|
223
|
+
primaryKey.push(col.name);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
name: tableName,
|
|
230
|
+
columns,
|
|
231
|
+
primaryKey,
|
|
232
|
+
foreignKeys,
|
|
233
|
+
indexes,
|
|
234
|
+
uniqueConstraints,
|
|
235
|
+
checkConstraints,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function generateMermaidER(tables: TableInfo[]): string {
|
|
240
|
+
const lines: string[] = ["erDiagram"];
|
|
241
|
+
|
|
242
|
+
for (const table of tables) {
|
|
243
|
+
lines.push(` ${table.name} {`);
|
|
244
|
+
for (const col of table.columns) {
|
|
245
|
+
const pk = table.primaryKey.includes(col.name) ? " PK" : "";
|
|
246
|
+
const fk = table.foreignKeys.some((fk) => fk.columns.includes(col.name)) ? " FK" : "";
|
|
247
|
+
const type = col.type.replace(/\s+/g, "_").replace(/[()]/g, "");
|
|
248
|
+
lines.push(` ${type} ${col.name}${pk}${fk}`);
|
|
249
|
+
}
|
|
250
|
+
lines.push(" }");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Relationships
|
|
254
|
+
for (const table of tables) {
|
|
255
|
+
for (const fk of table.foreignKeys) {
|
|
256
|
+
const relLabel = fk.columns.join(", ");
|
|
257
|
+
// Determine cardinality heuristic
|
|
258
|
+
const isUnique =
|
|
259
|
+
table.uniqueConstraints.some((uc) =>
|
|
260
|
+
fk.columns.every((c) => uc.includes(c))
|
|
261
|
+
) ||
|
|
262
|
+
fk.columns.every((c) =>
|
|
263
|
+
table.columns.find((col) => col.name === c)?.isUnique
|
|
264
|
+
) ||
|
|
265
|
+
fk.columns.every((c) => table.primaryKey.includes(c));
|
|
266
|
+
|
|
267
|
+
const cardinality = isUnique ? "||--||" : "}o--||";
|
|
268
|
+
lines.push(
|
|
269
|
+
` ${table.name} ${cardinality} ${fk.referencedTable} : "${relLabel}"`
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return lines.join("\n");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function parseCreateIndexes(sql: string): IndexInfo[] {
|
|
278
|
+
const indexes: IndexInfo[] = [];
|
|
279
|
+
const regex =
|
|
280
|
+
/CREATE\s+(UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?\s+ON\s+`?(\w+)`?\s*\(([^)]+)\)/gi;
|
|
281
|
+
let match: RegExpExecArray | null;
|
|
282
|
+
while ((match = regex.exec(sql)) !== null) {
|
|
283
|
+
indexes.push({
|
|
284
|
+
name: match[2],
|
|
285
|
+
columns: match[4].split(",").map((c) => c.trim().replace(/[`"[\]]/g, "")),
|
|
286
|
+
unique: !!match[1],
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
return indexes;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export function schemaAnalyzer(sql: string): SchemaAnalysis {
|
|
293
|
+
// Split into individual statements
|
|
294
|
+
const statements = sql
|
|
295
|
+
.split(";")
|
|
296
|
+
.map((s) => s.trim())
|
|
297
|
+
.filter(Boolean);
|
|
298
|
+
|
|
299
|
+
const tables: TableInfo[] = [];
|
|
300
|
+
const standaloneIndexes: { tableName: string; index: IndexInfo }[] = [];
|
|
301
|
+
|
|
302
|
+
for (const stmt of statements) {
|
|
303
|
+
if (/^\s*CREATE\s+TABLE\b/i.test(stmt)) {
|
|
304
|
+
const table = parseTable(stmt);
|
|
305
|
+
if (table) tables.push(table);
|
|
306
|
+
}
|
|
307
|
+
if (/^\s*CREATE\s+(UNIQUE\s+)?INDEX\b/i.test(stmt)) {
|
|
308
|
+
const idxMatch = stmt.match(
|
|
309
|
+
/CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?`?(\w+)`?\s+ON\s+`?(\w+)`?\s*\(([^)]+)\)/i
|
|
310
|
+
);
|
|
311
|
+
if (idxMatch) {
|
|
312
|
+
standaloneIndexes.push({
|
|
313
|
+
tableName: idxMatch[2],
|
|
314
|
+
index: {
|
|
315
|
+
name: idxMatch[1],
|
|
316
|
+
columns: idxMatch[3].split(",").map((c) => c.trim().replace(/[`"[\]]/g, "")),
|
|
317
|
+
unique: /UNIQUE/i.test(stmt),
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Attach standalone indexes to their tables
|
|
325
|
+
for (const si of standaloneIndexes) {
|
|
326
|
+
const table = tables.find(
|
|
327
|
+
(t) => t.name.toLowerCase() === si.tableName.toLowerCase()
|
|
328
|
+
);
|
|
329
|
+
if (table) {
|
|
330
|
+
table.indexes.push(si.index);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Build relationships
|
|
335
|
+
const relationships = tables.flatMap((t) =>
|
|
336
|
+
t.foreignKeys.map((fk) => ({
|
|
337
|
+
from: t.name,
|
|
338
|
+
fromColumns: fk.columns,
|
|
339
|
+
to: fk.referencedTable,
|
|
340
|
+
toColumns: fk.referencedColumns,
|
|
341
|
+
type: fk.columns.every((c) => t.primaryKey.includes(c))
|
|
342
|
+
? "one-to-one"
|
|
343
|
+
: "many-to-one",
|
|
344
|
+
}))
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
const mermaidDiagram = generateMermaidER(tables);
|
|
348
|
+
|
|
349
|
+
const totalColumns = tables.reduce((sum, t) => sum + t.columns.length, 0);
|
|
350
|
+
const totalFKs = tables.reduce((sum, t) => sum + t.foreignKeys.length, 0);
|
|
351
|
+
const totalIndexes = tables.reduce((sum, t) => sum + t.indexes.length, 0);
|
|
352
|
+
|
|
353
|
+
const summary = [
|
|
354
|
+
`Schema contains ${tables.length} table(s) with ${totalColumns} total column(s).`,
|
|
355
|
+
totalFKs > 0
|
|
356
|
+
? `${totalFKs} foreign key relationship(s) found.`
|
|
357
|
+
: "No foreign keys defined.",
|
|
358
|
+
totalIndexes > 0
|
|
359
|
+
? `${totalIndexes} index(es) defined.`
|
|
360
|
+
: "No standalone indexes defined.",
|
|
361
|
+
`Tables: ${tables.map((t) => t.name).join(", ")}`,
|
|
362
|
+
].join(" ");
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
tables,
|
|
366
|
+
relationships,
|
|
367
|
+
mermaidDiagram,
|
|
368
|
+
summary,
|
|
369
|
+
};
|
|
370
|
+
}
|