@arvoretech/db-diagram-mcp 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.
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseDDL, extractInlineForeignKeys } from "./parser.js";
3
+ import {
4
+ generateErd,
5
+ generateDomainMap,
6
+ explainTable,
7
+ traceFlow,
8
+ } from "./mermaid.js";
9
+
10
+ const sampleDDL = `
11
+ CREATE TABLE users (
12
+ id INT NOT NULL AUTO_INCREMENT,
13
+ email VARCHAR(255) NOT NULL,
14
+ name VARCHAR(100),
15
+ PRIMARY KEY (id),
16
+ UNIQUE KEY (email)
17
+ );
18
+
19
+ CREATE TABLE posts (
20
+ id INT NOT NULL AUTO_INCREMENT,
21
+ user_id INT NOT NULL,
22
+ title VARCHAR(255) NOT NULL,
23
+ PRIMARY KEY (id),
24
+ FOREIGN KEY (user_id) REFERENCES users(id)
25
+ );
26
+
27
+ CREATE TABLE comments (
28
+ id INT NOT NULL AUTO_INCREMENT,
29
+ post_id INT NOT NULL,
30
+ user_id INT NOT NULL,
31
+ content TEXT NOT NULL,
32
+ PRIMARY KEY (id),
33
+ FOREIGN KEY (post_id) REFERENCES posts(id),
34
+ FOREIGN KEY (user_id) REFERENCES users(id)
35
+ );
36
+ `;
37
+
38
+ function getSchema() {
39
+ let schema = parseDDL(sampleDDL);
40
+ schema = extractInlineForeignKeys(sampleDDL, schema);
41
+ return schema;
42
+ }
43
+
44
+ describe("generateErd", () => {
45
+ it("generates valid mermaid erDiagram", () => {
46
+ const schema = getSchema();
47
+ const diagram = generateErd(schema);
48
+ expect(diagram).toContain("erDiagram");
49
+ expect(diagram).toContain("users {");
50
+ expect(diagram).toContain("posts {");
51
+ expect(diagram).toContain("comments {");
52
+ });
53
+
54
+ it("includes relationship lines", () => {
55
+ const schema = getSchema();
56
+ const diagram = generateErd(schema);
57
+ expect(diagram).toContain("users");
58
+ expect(diagram).toContain("posts");
59
+ });
60
+
61
+ it("filters to specific tables", () => {
62
+ const schema = getSchema();
63
+ const diagram = generateErd(schema, { tables: ["users", "posts"] });
64
+ expect(diagram).toContain("users {");
65
+ expect(diagram).toContain("posts {");
66
+ expect(diagram).not.toContain("comments {");
67
+ });
68
+
69
+ it("adds title when provided", () => {
70
+ const schema = getSchema();
71
+ const diagram = generateErd(schema, { title: "My ERD" });
72
+ expect(diagram).toContain("title: My ERD");
73
+ });
74
+ });
75
+
76
+ describe("generateDomainMap", () => {
77
+ it("traverses from entry table", () => {
78
+ const schema = getSchema();
79
+ const diagram = generateDomainMap(schema, "posts", 1);
80
+ expect(diagram).toContain("posts {");
81
+ expect(diagram).toContain("users {");
82
+ expect(diagram).toContain("comments {");
83
+ });
84
+
85
+ it("respects depth limit", () => {
86
+ const schema = getSchema();
87
+ const diagram = generateDomainMap(schema, "comments", 0);
88
+ expect(diagram).toContain("comments {");
89
+ });
90
+ });
91
+
92
+ describe("explainTable", () => {
93
+ it("shows column details", () => {
94
+ const schema = getSchema();
95
+ const explanation = explainTable(schema, "posts");
96
+ expect(explanation).toContain("# posts");
97
+ expect(explanation).toContain("user_id");
98
+ expect(explanation).toContain("## Foreign Keys");
99
+ expect(explanation).toContain("users");
100
+ });
101
+
102
+ it("shows incoming references", () => {
103
+ const schema = getSchema();
104
+ const explanation = explainTable(schema, "users");
105
+ expect(explanation).toContain("## Referenced By");
106
+ expect(explanation).toContain("posts");
107
+ expect(explanation).toContain("comments");
108
+ });
109
+
110
+ it("returns error for unknown table", () => {
111
+ const schema = getSchema();
112
+ const explanation = explainTable(schema, "nonexistent");
113
+ expect(explanation).toContain("not found");
114
+ });
115
+ });
116
+
117
+ describe("traceFlow", () => {
118
+ it("finds path between tables", () => {
119
+ const schema = getSchema();
120
+ const flow = traceFlow(schema, "comments", "users");
121
+ expect(flow).toContain("# Flow:");
122
+ expect(flow).toContain("comments");
123
+ expect(flow).toContain("users");
124
+ });
125
+
126
+ it("returns message when no path exists", () => {
127
+ const ddl = `
128
+ CREATE TABLE a (id INT PRIMARY KEY);
129
+ CREATE TABLE b (id INT PRIMARY KEY);
130
+ `;
131
+ const schema = parseDDL(ddl);
132
+ const flow = traceFlow(schema, "a", "b");
133
+ expect(flow).toContain("No relationship path found");
134
+ });
135
+ });
package/src/mermaid.ts ADDED
@@ -0,0 +1,250 @@
1
+ import { Schema, Table } from "./types.js";
2
+
3
+ export function generateErd(
4
+ schema: Schema,
5
+ options?: { tables?: string[]; title?: string }
6
+ ): string {
7
+ const filteredTables = options?.tables
8
+ ? schema.tables.filter((t) => options.tables!.includes(t.name))
9
+ : schema.tables;
10
+
11
+ if (filteredTables.length === 0) return "erDiagram";
12
+
13
+ const lines: string[] = [];
14
+ if (options?.title) lines.push(`---\ntitle: ${options.title}\n---`);
15
+ lines.push("erDiagram");
16
+
17
+ for (const table of filteredTables) {
18
+ lines.push(` ${table.name} {`);
19
+ for (const col of table.columns) {
20
+ const markers: string[] = [];
21
+ if (col.primaryKey) markers.push("PK");
22
+ if (col.unique && !col.primaryKey) markers.push("UK");
23
+
24
+ const isFk = schema.tables.some((t) =>
25
+ t.foreignKeys.some(
26
+ (fk) =>
27
+ fk.referencedTable === table.name &&
28
+ fk.referencedColumns.includes(col.name)
29
+ )
30
+ );
31
+ const isLocalFk = table.foreignKeys.some((fk) =>
32
+ fk.columns.includes(col.name)
33
+ );
34
+ if (isFk || isLocalFk) markers.push("FK");
35
+
36
+ const markerStr = markers.length > 0 ? ` ${markers.join(",")}` : "";
37
+ lines.push(
38
+ ` ${normalizeType(col.type)} ${col.name}${markerStr}`
39
+ );
40
+ }
41
+ lines.push(" }");
42
+ }
43
+
44
+ const tableNames = new Set(filteredTables.map((t) => t.name));
45
+ for (const table of filteredTables) {
46
+ for (const fk of table.foreignKeys) {
47
+ if (!tableNames.has(fk.referencedTable)) continue;
48
+
49
+ const cardinality = inferCardinality(table, fk.columns);
50
+ lines.push(
51
+ ` ${fk.referencedTable} ${cardinality} ${table.name} : "${fk.columns.join(", ")}"`
52
+ );
53
+ }
54
+ }
55
+
56
+ return lines.join("\n");
57
+ }
58
+
59
+ export function generateDomainMap(
60
+ schema: Schema,
61
+ entryTable: string,
62
+ depth: number
63
+ ): string {
64
+ const visited = new Set<string>();
65
+ const queue: Array<{ table: string; level: number }> = [
66
+ { table: entryTable, level: 0 },
67
+ ];
68
+
69
+ while (queue.length > 0) {
70
+ const current = queue.shift()!;
71
+ if (visited.has(current.table) || current.level > depth) continue;
72
+ visited.add(current.table);
73
+
74
+ const table = schema.tables.find((t) => t.name === current.table);
75
+ if (!table) continue;
76
+
77
+ for (const fk of table.foreignKeys) {
78
+ if (!visited.has(fk.referencedTable)) {
79
+ queue.push({ table: fk.referencedTable, level: current.level + 1 });
80
+ }
81
+ }
82
+
83
+ for (const other of schema.tables) {
84
+ for (const fk of other.foreignKeys) {
85
+ if (
86
+ fk.referencedTable === current.table &&
87
+ !visited.has(other.name)
88
+ ) {
89
+ queue.push({ table: other.name, level: current.level + 1 });
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ const filteredSchema: Schema = {
96
+ tables: schema.tables.filter((t) => visited.has(t.name)),
97
+ };
98
+
99
+ return generateErd(filteredSchema, {
100
+ title: `Domain: ${entryTable} (depth ${depth})`,
101
+ });
102
+ }
103
+
104
+ export function explainTable(schema: Schema, tableName: string): string {
105
+ const table = schema.tables.find((t) => t.name === tableName);
106
+ if (!table) return `Table "${tableName}" not found in schema.`;
107
+
108
+ const lines: string[] = [`# ${tableName}`, ""];
109
+
110
+ lines.push("## Columns", "");
111
+ lines.push("| Column | Type | PK | Nullable | Unique | Default |");
112
+ lines.push("|--------|------|----|---------:|--------|---------|");
113
+ for (const col of table.columns) {
114
+ lines.push(
115
+ `| ${col.name} | ${col.type} | ${col.primaryKey ? "✓" : ""} | ${col.nullable ? "✓" : ""} | ${col.unique ? "✓" : ""} | ${col.defaultValue ?? ""} |`
116
+ );
117
+ }
118
+
119
+ if (table.foreignKeys.length > 0) {
120
+ lines.push("", "## Foreign Keys (outgoing)", "");
121
+ for (const fk of table.foreignKeys) {
122
+ lines.push(
123
+ `- ${fk.columns.join(", ")} → ${fk.referencedTable}(${fk.referencedColumns.join(", ")})`
124
+ );
125
+ }
126
+ }
127
+
128
+ const incomingFks = schema.tables.filter((t) =>
129
+ t.foreignKeys.some((fk) => fk.referencedTable === tableName)
130
+ );
131
+ if (incomingFks.length > 0) {
132
+ lines.push("", "## Referenced By (incoming)", "");
133
+ for (const t of incomingFks) {
134
+ for (const fk of t.foreignKeys) {
135
+ if (fk.referencedTable === tableName) {
136
+ lines.push(
137
+ `- ${t.name}(${fk.columns.join(", ")}) → ${tableName}(${fk.referencedColumns.join(", ")})`
138
+ );
139
+ }
140
+ }
141
+ }
142
+ }
143
+
144
+ const relatedTables = new Set<string>();
145
+ for (const fk of table.foreignKeys) relatedTables.add(fk.referencedTable);
146
+ for (const t of incomingFks) relatedTables.add(t.name);
147
+
148
+ if (relatedTables.size > 0) {
149
+ const miniSchema: Schema = {
150
+ tables: schema.tables.filter(
151
+ (t) => t.name === tableName || relatedTables.has(t.name)
152
+ ),
153
+ };
154
+ lines.push("", "## Diagram", "", "```mermaid");
155
+ lines.push(generateErd(miniSchema, { title: tableName }));
156
+ lines.push("```");
157
+ }
158
+
159
+ return lines.join("\n");
160
+ }
161
+
162
+ export function traceFlow(
163
+ schema: Schema,
164
+ from: string,
165
+ to: string
166
+ ): string {
167
+ const paths = findPaths(schema, from, to, 6);
168
+
169
+ if (paths.length === 0) {
170
+ return `No relationship path found between "${from}" and "${to}" within 6 hops.`;
171
+ }
172
+
173
+ const allTables = new Set<string>();
174
+ for (const path of paths) {
175
+ for (const table of path) allTables.add(table);
176
+ }
177
+
178
+ const lines: string[] = [
179
+ `# Flow: ${from} → ${to}`,
180
+ "",
181
+ `Found ${paths.length} path(s):`,
182
+ "",
183
+ ];
184
+
185
+ for (let i = 0; i < paths.length; i++) {
186
+ lines.push(`${i + 1}. ${paths[i].join(" → ")}`);
187
+ }
188
+
189
+ const filteredSchema: Schema = {
190
+ tables: schema.tables.filter((t) => allTables.has(t.name)),
191
+ };
192
+
193
+ lines.push("", "```mermaid");
194
+ lines.push(generateErd(filteredSchema, { title: `${from} → ${to}` }));
195
+ lines.push("```");
196
+
197
+ return lines.join("\n");
198
+ }
199
+
200
+ function findPaths(
201
+ schema: Schema,
202
+ from: string,
203
+ to: string,
204
+ maxDepth: number
205
+ ): string[][] {
206
+ const results: string[][] = [];
207
+
208
+ function dfs(current: string, target: string, path: string[]): void {
209
+ if (path.length > maxDepth) return;
210
+ if (current === target) {
211
+ results.push([...path]);
212
+ return;
213
+ }
214
+
215
+ const table = schema.tables.find((t) => t.name === current);
216
+ if (!table) return;
217
+
218
+ for (const fk of table.foreignKeys) {
219
+ if (!path.includes(fk.referencedTable)) {
220
+ path.push(fk.referencedTable);
221
+ dfs(fk.referencedTable, target, path);
222
+ path.pop();
223
+ }
224
+ }
225
+
226
+ for (const other of schema.tables) {
227
+ for (const fk of other.foreignKeys) {
228
+ if (fk.referencedTable === current && !path.includes(other.name)) {
229
+ path.push(other.name);
230
+ dfs(other.name, target, path);
231
+ path.pop();
232
+ }
233
+ }
234
+ }
235
+ }
236
+
237
+ dfs(from, to, [from]);
238
+ return results.slice(0, 5);
239
+ }
240
+
241
+ function inferCardinality(table: Table, fkColumns: string[]): string {
242
+ const hasUnique = table.columns.some(
243
+ (c) => fkColumns.includes(c.name) && (c.unique || c.primaryKey)
244
+ );
245
+ return hasUnique ? "||--||" : "||--o{";
246
+ }
247
+
248
+ function normalizeType(type: string): string {
249
+ return type.replace(/[(),\s]/g, "_").replace(/_+$/, "");
250
+ }
@@ -0,0 +1,118 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseDDL, extractInlineForeignKeys } from "./parser.js";
3
+
4
+ const sampleDDL = `
5
+ CREATE TABLE users (
6
+ id INT NOT NULL AUTO_INCREMENT,
7
+ email VARCHAR(255) NOT NULL,
8
+ name VARCHAR(100),
9
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
10
+ PRIMARY KEY (id),
11
+ UNIQUE KEY (email)
12
+ );
13
+
14
+ CREATE TABLE posts (
15
+ id INT NOT NULL AUTO_INCREMENT,
16
+ user_id INT NOT NULL,
17
+ title VARCHAR(255) NOT NULL,
18
+ body TEXT,
19
+ published_at TIMESTAMP,
20
+ PRIMARY KEY (id),
21
+ FOREIGN KEY (user_id) REFERENCES users(id)
22
+ );
23
+
24
+ CREATE TABLE comments (
25
+ id INT NOT NULL AUTO_INCREMENT,
26
+ post_id INT NOT NULL,
27
+ user_id INT NOT NULL,
28
+ content TEXT NOT NULL,
29
+ PRIMARY KEY (id),
30
+ FOREIGN KEY (post_id) REFERENCES posts(id),
31
+ FOREIGN KEY (user_id) REFERENCES users(id)
32
+ );
33
+
34
+ CREATE TABLE tags (
35
+ id INT NOT NULL AUTO_INCREMENT,
36
+ name VARCHAR(50) NOT NULL UNIQUE,
37
+ PRIMARY KEY (id)
38
+ );
39
+
40
+ CREATE TABLE post_tags (
41
+ post_id INT NOT NULL,
42
+ tag_id INT NOT NULL,
43
+ PRIMARY KEY (post_id, tag_id),
44
+ FOREIGN KEY (post_id) REFERENCES posts(id),
45
+ FOREIGN KEY (tag_id) REFERENCES tags(id)
46
+ );
47
+ `;
48
+
49
+ describe("parseDDL", () => {
50
+ it("parses all tables", () => {
51
+ const schema = parseDDL(sampleDDL);
52
+ expect(schema.tables).toHaveLength(5);
53
+ expect(schema.tables.map((t) => t.name)).toEqual([
54
+ "users",
55
+ "posts",
56
+ "comments",
57
+ "tags",
58
+ "post_tags",
59
+ ]);
60
+ });
61
+
62
+ it("parses columns correctly", () => {
63
+ const schema = parseDDL(sampleDDL);
64
+ const users = schema.tables.find((t) => t.name === "users")!;
65
+ expect(users.columns).toHaveLength(4);
66
+
67
+ const idCol = users.columns.find((c) => c.name === "id")!;
68
+ expect(idCol.primaryKey).toBe(true);
69
+ expect(idCol.nullable).toBe(false);
70
+
71
+ const emailCol = users.columns.find((c) => c.name === "email")!;
72
+ expect(emailCol.nullable).toBe(false);
73
+ });
74
+
75
+ it("parses foreign keys", () => {
76
+ const schema = parseDDL(sampleDDL);
77
+ const posts = schema.tables.find((t) => t.name === "posts")!;
78
+ expect(posts.foreignKeys).toHaveLength(1);
79
+ expect(posts.foreignKeys[0]).toEqual({
80
+ columns: ["user_id"],
81
+ referencedTable: "users",
82
+ referencedColumns: ["id"],
83
+ });
84
+ });
85
+
86
+ it("parses composite primary keys", () => {
87
+ const schema = parseDDL(sampleDDL);
88
+ const postTags = schema.tables.find((t) => t.name === "post_tags")!;
89
+ const pkCols = postTags.columns.filter((c) => c.primaryKey);
90
+ expect(pkCols.map((c) => c.name)).toEqual(["post_id", "tag_id"]);
91
+ });
92
+
93
+ it("parses multiple foreign keys on one table", () => {
94
+ const schema = parseDDL(sampleDDL);
95
+ const comments = schema.tables.find((t) => t.name === "comments")!;
96
+ expect(comments.foreignKeys).toHaveLength(2);
97
+ });
98
+
99
+ it("handles PostgreSQL inline references", () => {
100
+ const pgDDL = `
101
+ CREATE TABLE orders (
102
+ id SERIAL PRIMARY KEY,
103
+ customer_id INT NOT NULL REFERENCES customers(id),
104
+ total DECIMAL(10,2)
105
+ );
106
+
107
+ CREATE TABLE customers (
108
+ id SERIAL PRIMARY KEY,
109
+ name VARCHAR(100) NOT NULL
110
+ );
111
+ `;
112
+ let schema = parseDDL(pgDDL);
113
+ schema = extractInlineForeignKeys(pgDDL, schema);
114
+ const orders = schema.tables.find((t) => t.name === "orders")!;
115
+ expect(orders.foreignKeys).toHaveLength(1);
116
+ expect(orders.foreignKeys[0].referencedTable).toBe("customers");
117
+ });
118
+ });
package/src/parser.ts ADDED
@@ -0,0 +1,185 @@
1
+ import { Schema, Table, Column, ForeignKey } from "./types.js";
2
+
3
+ export function parseDDL(ddl: string): Schema {
4
+ const tables: Table[] = [];
5
+ const createTableRegex =
6
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?(\w+)[`"']?\s*\(([\s\S]*?)\)\s*(?:ENGINE|;|\n\n)/gi;
7
+
8
+ let match: RegExpExecArray | null;
9
+ while ((match = createTableRegex.exec(ddl)) !== null) {
10
+ const tableName = match[1];
11
+ const body = match[2];
12
+ const table = parseTableBody(tableName, body);
13
+ tables.push(table);
14
+ }
15
+
16
+ if (tables.length === 0) {
17
+ const simpleRegex =
18
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?(\w+)[`"']?\s*\(([\s\S]*?)\)/gi;
19
+ while ((match = simpleRegex.exec(ddl)) !== null) {
20
+ const tableName = match[1];
21
+ const body = match[2];
22
+ const table = parseTableBody(tableName, body);
23
+ tables.push(table);
24
+ }
25
+ }
26
+
27
+ return { tables };
28
+ }
29
+
30
+ function parseTableBody(tableName: string, body: string): Table {
31
+ const columns: Column[] = [];
32
+ const foreignKeys: ForeignKey[] = [];
33
+ const primaryKeyColumns: string[] = [];
34
+
35
+ const lines = splitTableBody(body);
36
+
37
+ for (const line of lines) {
38
+ const trimmed = line.trim();
39
+ if (!trimmed) continue;
40
+
41
+ const pkMatch = trimmed.match(
42
+ /^\s*(?:CONSTRAINT\s+[`"']?\w+[`"']?\s+)?PRIMARY\s+KEY\s*\(([^)]+)\)/i
43
+ );
44
+ if (pkMatch) {
45
+ const cols = pkMatch[1]
46
+ .split(",")
47
+ .map((c) => c.trim().replace(/[`"']/g, ""));
48
+ primaryKeyColumns.push(...cols);
49
+ continue;
50
+ }
51
+
52
+ const fkMatch = trimmed.match(
53
+ /^\s*(?:CONSTRAINT\s+[`"']?\w+[`"']?\s+)?FOREIGN\s+KEY\s*\(([^)]+)\)\s*REFERENCES\s+[`"']?(\w+)[`"']?\s*\(([^)]+)\)/i
54
+ );
55
+ if (fkMatch) {
56
+ foreignKeys.push({
57
+ columns: fkMatch[1]
58
+ .split(",")
59
+ .map((c) => c.trim().replace(/[`"']/g, "")),
60
+ referencedTable: fkMatch[2],
61
+ referencedColumns: fkMatch[3]
62
+ .split(",")
63
+ .map((c) => c.trim().replace(/[`"']/g, "")),
64
+ });
65
+ continue;
66
+ }
67
+
68
+ const uniqueConstraint = trimmed.match(
69
+ /^\s*(?:CONSTRAINT\s+[`"']?\w+[`"']?\s+)?UNIQUE\s+(?:KEY|INDEX)?\s*(?:[`"']?\w+[`"']?\s*)?\(([^)]+)\)/i
70
+ );
71
+ if (uniqueConstraint) continue;
72
+
73
+ const indexMatch = trimmed.match(/^\s*(?:KEY|INDEX)\s/i);
74
+ if (indexMatch) continue;
75
+
76
+ const column = parseColumn(trimmed);
77
+ if (column) columns.push(column);
78
+ }
79
+
80
+ for (const col of columns) {
81
+ if (primaryKeyColumns.includes(col.name)) {
82
+ col.primaryKey = true;
83
+ }
84
+ }
85
+
86
+ return { name: tableName, columns, foreignKeys };
87
+ }
88
+
89
+ function splitTableBody(body: string): string[] {
90
+ const lines: string[] = [];
91
+ let current = "";
92
+ let parenDepth = 0;
93
+
94
+ for (const char of body) {
95
+ if (char === "(") parenDepth++;
96
+ if (char === ")") parenDepth--;
97
+ if (char === "," && parenDepth === 0) {
98
+ lines.push(current);
99
+ current = "";
100
+ } else {
101
+ current += char;
102
+ }
103
+ }
104
+ if (current.trim()) lines.push(current);
105
+
106
+ return lines;
107
+ }
108
+
109
+ function parseColumn(line: string): Column | null {
110
+ const match = line.match(
111
+ /^\s*[`"']?(\w+)[`"']?\s+([\w(),.]+(?:\s*\(\d+(?:,\s*\d+)?\))?)/i
112
+ );
113
+ if (!match) return null;
114
+
115
+ const name = match[1];
116
+ const type = match[2].toUpperCase();
117
+
118
+ const upperLine = line.toUpperCase();
119
+ const nullable = !upperLine.includes("NOT NULL");
120
+ const primaryKey =
121
+ upperLine.includes("PRIMARY KEY") ||
122
+ (upperLine.includes("SERIAL") && !upperLine.includes("BIGSERIAL"));
123
+ const unique =
124
+ upperLine.includes("UNIQUE") || upperLine.includes("PRIMARY KEY");
125
+
126
+ let defaultValue: string | undefined;
127
+ const defaultMatch = line.match(/DEFAULT\s+([^,\s]+)/i);
128
+ if (defaultMatch) defaultValue = defaultMatch[1].replace(/['"]/g, "");
129
+
130
+ const inlineFkMatch = line.match(
131
+ /REFERENCES\s+[`"']?(\w+)[`"']?\s*\(([^)]+)\)/i
132
+ );
133
+
134
+ return {
135
+ name,
136
+ type,
137
+ nullable,
138
+ primaryKey,
139
+ unique,
140
+ defaultValue,
141
+ ...(inlineFkMatch ? { _inlineRef: inlineFkMatch } : {}),
142
+ } as Column;
143
+ }
144
+
145
+ export function extractInlineForeignKeys(ddl: string, schema: Schema): Schema {
146
+ const createTableRegex =
147
+ /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?(\w+)[`"']?\s*\(([\s\S]*?)\)(?:\s*(?:ENGINE|;|\n\n))/gi;
148
+
149
+ let match: RegExpExecArray | null;
150
+ while ((match = createTableRegex.exec(ddl)) !== null) {
151
+ const tableName = match[1];
152
+ const body = match[2];
153
+ const table = schema.tables.find((t) => t.name === tableName);
154
+ if (!table) continue;
155
+
156
+ const lines = splitTableBody(body);
157
+ for (const line of lines) {
158
+ const colMatch = line.match(/^\s*[`"']?(\w+)[`"']?\s+/);
159
+ const fkMatch = line.match(
160
+ /REFERENCES\s+[`"']?(\w+)[`"']?\s*\(([^)]+)\)/i
161
+ );
162
+ if (colMatch && fkMatch) {
163
+ const colName = colMatch[1].replace(/[`"']/g, "");
164
+ const refTable = fkMatch[1];
165
+ const refCols = fkMatch[2]
166
+ .split(",")
167
+ .map((c) => c.trim().replace(/[`"']/g, ""));
168
+
169
+ const exists = table.foreignKeys.some(
170
+ (fk) =>
171
+ fk.columns.includes(colName) && fk.referencedTable === refTable
172
+ );
173
+ if (!exists) {
174
+ table.foreignKeys.push({
175
+ columns: [colName],
176
+ referencedTable: refTable,
177
+ referencedColumns: refCols,
178
+ });
179
+ }
180
+ }
181
+ }
182
+ }
183
+
184
+ return schema;
185
+ }