@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.
@@ -0,0 +1,573 @@
1
+ /**
2
+ * SQL Explainer — take a SQL query and explain what it does
3
+ * in plain English, step by step.
4
+ */
5
+
6
+ interface ExplanationStep {
7
+ clause: string;
8
+ sql: string;
9
+ explanation: string;
10
+ }
11
+
12
+ interface ExplanationResult {
13
+ summary: string;
14
+ queryType: string;
15
+ steps: ExplanationStep[];
16
+ tables: string[];
17
+ columns: string[];
18
+ conditions: string[];
19
+ tips: string[];
20
+ }
21
+
22
+ function normalizeWhitespace(sql: string): string {
23
+ return sql.replace(/\s+/g, " ").trim();
24
+ }
25
+
26
+ function detectQueryType(sql: string): string {
27
+ const upper = sql.trim().toUpperCase();
28
+ if (upper.startsWith("SELECT")) return "SELECT";
29
+ if (upper.startsWith("INSERT")) return "INSERT";
30
+ if (upper.startsWith("UPDATE")) return "UPDATE";
31
+ if (upper.startsWith("DELETE")) return "DELETE";
32
+ if (upper.startsWith("CREATE TABLE")) return "CREATE TABLE";
33
+ if (upper.startsWith("ALTER TABLE")) return "ALTER TABLE";
34
+ if (upper.startsWith("DROP")) return "DROP";
35
+ if (upper.startsWith("WITH")) return "CTE (Common Table Expression)";
36
+ if (upper.startsWith("CREATE INDEX")) return "CREATE INDEX";
37
+ if (upper.startsWith("TRUNCATE")) return "TRUNCATE";
38
+ return "UNKNOWN";
39
+ }
40
+
41
+ function extractTables(sql: string): string[] {
42
+ const tables: Set<string> = new Set();
43
+ const upper = normalizeWhitespace(sql).toUpperCase();
44
+ const original = normalizeWhitespace(sql);
45
+
46
+ // FROM table
47
+ const fromRegex = /\bFROM\s+(\w+(?:\.\w+)?)/gi;
48
+ let match: RegExpExecArray | null;
49
+ while ((match = fromRegex.exec(original)) !== null) {
50
+ tables.add(match[1]);
51
+ }
52
+
53
+ // JOIN table
54
+ const joinRegex = /\bJOIN\s+(\w+(?:\.\w+)?)/gi;
55
+ while ((match = joinRegex.exec(original)) !== null) {
56
+ tables.add(match[1]);
57
+ }
58
+
59
+ // INSERT INTO table
60
+ const insertRegex = /\bINSERT\s+INTO\s+(\w+(?:\.\w+)?)/gi;
61
+ while ((match = insertRegex.exec(original)) !== null) {
62
+ tables.add(match[1]);
63
+ }
64
+
65
+ // UPDATE table
66
+ const updateRegex = /\bUPDATE\s+(\w+(?:\.\w+)?)/gi;
67
+ while ((match = updateRegex.exec(original)) !== null) {
68
+ tables.add(match[1]);
69
+ }
70
+
71
+ // DELETE FROM table
72
+ const deleteRegex = /\bDELETE\s+FROM\s+(\w+(?:\.\w+)?)/gi;
73
+ while ((match = deleteRegex.exec(original)) !== null) {
74
+ tables.add(match[1]);
75
+ }
76
+
77
+ // CREATE TABLE table
78
+ const createRegex = /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+(?:\.\w+)?)/gi;
79
+ while ((match = createRegex.exec(original)) !== null) {
80
+ tables.add(match[1]);
81
+ }
82
+
83
+ // Ignore SQL keywords that might be captured
84
+ const ignore = new Set([
85
+ "SELECT", "SET", "WHERE", "VALUES", "DEFAULT", "NULL",
86
+ "NOT", "EXISTS", "TABLE", "INDEX", "VIEW",
87
+ ]);
88
+ return Array.from(tables).filter((t) => !ignore.has(t.toUpperCase()));
89
+ }
90
+
91
+ function extractColumns(sql: string): string[] {
92
+ const cols: Set<string> = new Set();
93
+ const norm = normalizeWhitespace(sql);
94
+
95
+ // SELECT columns FROM
96
+ const selectMatch = norm.match(/\bSELECT\s+([\s\S]*?)\s+FROM\b/i);
97
+ if (selectMatch) {
98
+ const colStr = selectMatch[1];
99
+ if (colStr.trim() !== "*") {
100
+ splitRespectingParens(colStr).forEach((c) => {
101
+ const cleaned = c.trim().replace(/\s+AS\s+\w+$/i, "").trim();
102
+ cols.add(cleaned);
103
+ });
104
+ } else {
105
+ cols.add("*");
106
+ }
107
+ }
108
+
109
+ return Array.from(cols);
110
+ }
111
+
112
+ function splitRespectingParens(text: string): string[] {
113
+ const parts: string[] = [];
114
+ let depth = 0;
115
+ let current = "";
116
+ for (const ch of text) {
117
+ if (ch === "(") depth++;
118
+ if (ch === ")") depth--;
119
+ if (ch === "," && depth === 0) {
120
+ parts.push(current.trim());
121
+ current = "";
122
+ continue;
123
+ }
124
+ current += ch;
125
+ }
126
+ if (current.trim()) parts.push(current.trim());
127
+ return parts;
128
+ }
129
+
130
+ function extractConditions(sql: string): string[] {
131
+ const conditions: string[] = [];
132
+ const norm = normalizeWhitespace(sql);
133
+
134
+ // WHERE clause
135
+ const whereMatch = norm.match(
136
+ /\bWHERE\s+([\s\S]*?)(?:\bGROUP BY\b|\bORDER BY\b|\bLIMIT\b|\bHAVING\b|\bUNION\b|$)/i
137
+ );
138
+ if (whereMatch) {
139
+ conditions.push(`WHERE: ${whereMatch[1].trim()}`);
140
+ }
141
+
142
+ // HAVING clause
143
+ const havingMatch = norm.match(
144
+ /\bHAVING\s+([\s\S]*?)(?:\bORDER BY\b|\bLIMIT\b|\bUNION\b|$)/i
145
+ );
146
+ if (havingMatch) {
147
+ conditions.push(`HAVING: ${havingMatch[1].trim()}`);
148
+ }
149
+
150
+ // ON conditions from JOINs
151
+ const onRegex = /\bON\s+([\w.]+\s*=\s*[\w.]+)/gi;
152
+ let match: RegExpExecArray | null;
153
+ while ((match = onRegex.exec(norm)) !== null) {
154
+ conditions.push(`JOIN ON: ${match[1].trim()}`);
155
+ }
156
+
157
+ return conditions;
158
+ }
159
+
160
+ function explainSelect(sql: string): ExplanationStep[] {
161
+ const steps: ExplanationStep[] = [];
162
+ const norm = normalizeWhitespace(sql);
163
+
164
+ // WITH clause
165
+ const withMatch = norm.match(/^WITH\s+([\s\S]*?)\s+SELECT/i);
166
+ if (withMatch) {
167
+ const cteNames = withMatch[1].match(/(\w+)\s+AS\s*\(/gi) || [];
168
+ steps.push({
169
+ clause: "WITH (CTE)",
170
+ sql: `WITH ${withMatch[1].substring(0, 80)}...`,
171
+ explanation: `Defines ${cteNames.length} Common Table Expression(s) as temporary named result sets: ${cteNames.map((c) => c.replace(/\s+AS\s*\(/i, "")).join(", ")}`,
172
+ });
173
+ }
174
+
175
+ // SELECT
176
+ const selectMatch = norm.match(/\bSELECT\s+(DISTINCT\s+)?([\s\S]*?)\s+FROM\b/i);
177
+ if (selectMatch) {
178
+ const distinct = selectMatch[1] ? "unique " : "";
179
+ const cols = selectMatch[2].trim();
180
+ const colList = cols === "*" ? "all columns" : `specific columns: ${cols}`;
181
+ steps.push({
182
+ clause: "SELECT",
183
+ sql: `SELECT ${selectMatch[1] || ""}${cols}`,
184
+ explanation: `Retrieves ${distinct}${colList} from the data source`,
185
+ });
186
+ }
187
+
188
+ // FROM
189
+ const fromMatch = norm.match(/\bFROM\s+(\w+(?:\.\w+)?(?:\s+(?:AS\s+)?\w+)?)/i);
190
+ if (fromMatch) {
191
+ steps.push({
192
+ clause: "FROM",
193
+ sql: `FROM ${fromMatch[1]}`,
194
+ explanation: `Reads data from the "${fromMatch[1].split(/\s+/)[0]}" table`,
195
+ });
196
+ }
197
+
198
+ // JOINs
199
+ const joinRegex =
200
+ /\b((?:INNER|LEFT|RIGHT|FULL|CROSS)\s+(?:OUTER\s+)?)?JOIN\s+(\w+(?:\.\w+)?(?:\s+(?:AS\s+)?\w+)?)\s+ON\s+([\s\S]*?)(?=\b(?:INNER|LEFT|RIGHT|FULL|CROSS)?\s*JOIN\b|\bWHERE\b|\bGROUP\b|\bORDER\b|\bLIMIT\b|\bHAVING\b|$)/gi;
201
+ let jmatch: RegExpExecArray | null;
202
+ while ((jmatch = joinRegex.exec(norm)) !== null) {
203
+ const joinType = (jmatch[1] || "INNER").trim().toUpperCase();
204
+ const table = jmatch[2].split(/\s+/)[0];
205
+ const condition = jmatch[3].trim();
206
+ const joinDescriptions: Record<string, string> = {
207
+ INNER: "matching rows from both tables",
208
+ LEFT: "all rows from the left table and matching rows from the right",
209
+ "LEFT OUTER": "all rows from the left table and matching rows from the right",
210
+ RIGHT: "all rows from the right table and matching rows from the left",
211
+ "RIGHT OUTER": "all rows from the right table and matching rows from the left",
212
+ FULL: "all rows from both tables, with NULLs where there is no match",
213
+ "FULL OUTER": "all rows from both tables, with NULLs where there is no match",
214
+ CROSS: "every combination of rows from both tables (cartesian product)",
215
+ };
216
+ const desc = joinDescriptions[joinType] || "matching rows";
217
+ steps.push({
218
+ clause: `${joinType} JOIN`,
219
+ sql: `${joinType} JOIN ${jmatch[2]} ON ${condition}`,
220
+ explanation: `Combines with "${table}" table, keeping ${desc}, linked by ${condition}`,
221
+ });
222
+ }
223
+
224
+ // WHERE
225
+ const whereMatch = norm.match(
226
+ /\bWHERE\s+([\s\S]*?)(?=\bGROUP BY\b|\bORDER BY\b|\bLIMIT\b|\bHAVING\b|\bUNION\b|$)/i
227
+ );
228
+ if (whereMatch) {
229
+ steps.push({
230
+ clause: "WHERE",
231
+ sql: `WHERE ${whereMatch[1].trim()}`,
232
+ explanation: `Filters rows to only include those where: ${describeConditions(whereMatch[1].trim())}`,
233
+ });
234
+ }
235
+
236
+ // GROUP BY
237
+ const groupMatch = norm.match(
238
+ /\bGROUP BY\s+([\s\S]*?)(?=\bHAVING\b|\bORDER BY\b|\bLIMIT\b|\bUNION\b|$)/i
239
+ );
240
+ if (groupMatch) {
241
+ steps.push({
242
+ clause: "GROUP BY",
243
+ sql: `GROUP BY ${groupMatch[1].trim()}`,
244
+ explanation: `Groups rows together by ${groupMatch[1].trim()}, collapsing multiple rows into one per group`,
245
+ });
246
+ }
247
+
248
+ // HAVING
249
+ const havingMatch = norm.match(
250
+ /\bHAVING\s+([\s\S]*?)(?=\bORDER BY\b|\bLIMIT\b|\bUNION\b|$)/i
251
+ );
252
+ if (havingMatch) {
253
+ steps.push({
254
+ clause: "HAVING",
255
+ sql: `HAVING ${havingMatch[1].trim()}`,
256
+ explanation: `Filters the grouped results to only include groups where: ${havingMatch[1].trim()}`,
257
+ });
258
+ }
259
+
260
+ // ORDER BY
261
+ const orderMatch = norm.match(
262
+ /\bORDER BY\s+([\s\S]*?)(?=\bLIMIT\b|\bOFFSET\b|\bFETCH\b|\bUNION\b|$)/i
263
+ );
264
+ if (orderMatch) {
265
+ steps.push({
266
+ clause: "ORDER BY",
267
+ sql: `ORDER BY ${orderMatch[1].trim()}`,
268
+ explanation: `Sorts the results by ${orderMatch[1].trim()}`,
269
+ });
270
+ }
271
+
272
+ // LIMIT / OFFSET
273
+ const limitMatch = norm.match(/\bLIMIT\s+(\d+)/i);
274
+ const offsetMatch = norm.match(/\bOFFSET\s+(\d+)/i);
275
+ if (limitMatch) {
276
+ let desc = `Returns at most ${limitMatch[1]} row(s)`;
277
+ if (offsetMatch) {
278
+ desc += `, skipping the first ${offsetMatch[1]}`;
279
+ }
280
+ steps.push({
281
+ clause: "LIMIT",
282
+ sql: `LIMIT ${limitMatch[1]}${offsetMatch ? ` OFFSET ${offsetMatch[1]}` : ""}`,
283
+ explanation: desc,
284
+ });
285
+ }
286
+
287
+ return steps;
288
+ }
289
+
290
+ function describeConditions(cond: string): string {
291
+ // Simple heuristic descriptions
292
+ return cond
293
+ .replace(/\bAND\b/gi, " AND ")
294
+ .replace(/\bOR\b/gi, " OR ")
295
+ .replace(/\s+/g, " ")
296
+ .trim();
297
+ }
298
+
299
+ function explainInsert(sql: string): ExplanationStep[] {
300
+ const steps: ExplanationStep[] = [];
301
+ const norm = normalizeWhitespace(sql);
302
+
303
+ const tableMatch = norm.match(/\bINSERT\s+INTO\s+(\w+(?:\.\w+)?)/i);
304
+ if (tableMatch) {
305
+ steps.push({
306
+ clause: "INSERT INTO",
307
+ sql: `INSERT INTO ${tableMatch[1]}`,
308
+ explanation: `Adds new row(s) to the "${tableMatch[1]}" table`,
309
+ });
310
+ }
311
+
312
+ const colsMatch = norm.match(/\bINSERT\s+INTO\s+\w+\s*\(([^)]+)\)/i);
313
+ if (colsMatch) {
314
+ steps.push({
315
+ clause: "COLUMNS",
316
+ sql: `(${colsMatch[1]})`,
317
+ explanation: `Specifying values for columns: ${colsMatch[1]}`,
318
+ });
319
+ }
320
+
321
+ if (/\bVALUES\b/i.test(norm)) {
322
+ const valuesCount = (norm.match(/\)\s*,\s*\(/g) || []).length + 1;
323
+ steps.push({
324
+ clause: "VALUES",
325
+ sql: "VALUES (...)",
326
+ explanation: `Inserting ${valuesCount} row(s) of literal values`,
327
+ });
328
+ }
329
+
330
+ if (/\bSELECT\b/i.test(norm) && /\bINSERT\b/i.test(norm)) {
331
+ steps.push({
332
+ clause: "SELECT (subquery)",
333
+ sql: "INSERT ... SELECT",
334
+ explanation: "Inserting rows from the result of a SELECT query",
335
+ });
336
+ }
337
+
338
+ const returningMatch = norm.match(/\bRETURNING\s+(.*)/i);
339
+ if (returningMatch) {
340
+ steps.push({
341
+ clause: "RETURNING",
342
+ sql: `RETURNING ${returningMatch[1]}`,
343
+ explanation: `Returns ${returningMatch[1]} from the newly inserted rows`,
344
+ });
345
+ }
346
+
347
+ return steps;
348
+ }
349
+
350
+ function explainUpdate(sql: string): ExplanationStep[] {
351
+ const steps: ExplanationStep[] = [];
352
+ const norm = normalizeWhitespace(sql);
353
+
354
+ const tableMatch = norm.match(/\bUPDATE\s+(\w+(?:\.\w+)?)/i);
355
+ if (tableMatch) {
356
+ steps.push({
357
+ clause: "UPDATE",
358
+ sql: `UPDATE ${tableMatch[1]}`,
359
+ explanation: `Modifies existing rows in the "${tableMatch[1]}" table`,
360
+ });
361
+ }
362
+
363
+ const setMatch = norm.match(/\bSET\s+([\s\S]*?)(?=\bWHERE\b|\bRETURNING\b|$)/i);
364
+ if (setMatch) {
365
+ const assignments = splitRespectingParens(setMatch[1]);
366
+ steps.push({
367
+ clause: "SET",
368
+ sql: `SET ${setMatch[1].trim()}`,
369
+ explanation: `Changes ${assignments.length} column(s): ${assignments.map((a) => a.split("=")[0].trim()).join(", ")}`,
370
+ });
371
+ }
372
+
373
+ const whereMatch = norm.match(/\bWHERE\s+([\s\S]*?)(?=\bRETURNING\b|$)/i);
374
+ if (whereMatch) {
375
+ steps.push({
376
+ clause: "WHERE",
377
+ sql: `WHERE ${whereMatch[1].trim()}`,
378
+ explanation: `Only updates rows where: ${whereMatch[1].trim()}`,
379
+ });
380
+ } else {
381
+ steps.push({
382
+ clause: "WARNING",
383
+ sql: "No WHERE clause",
384
+ explanation: "WARNING: This updates ALL rows in the table! Consider adding a WHERE clause.",
385
+ });
386
+ }
387
+
388
+ return steps;
389
+ }
390
+
391
+ function explainDelete(sql: string): ExplanationStep[] {
392
+ const steps: ExplanationStep[] = [];
393
+ const norm = normalizeWhitespace(sql);
394
+
395
+ const tableMatch = norm.match(/\bDELETE\s+FROM\s+(\w+(?:\.\w+)?)/i);
396
+ if (tableMatch) {
397
+ steps.push({
398
+ clause: "DELETE FROM",
399
+ sql: `DELETE FROM ${tableMatch[1]}`,
400
+ explanation: `Removes rows from the "${tableMatch[1]}" table`,
401
+ });
402
+ }
403
+
404
+ const whereMatch = norm.match(/\bWHERE\s+([\s\S]*?)(?=\bRETURNING\b|$)/i);
405
+ if (whereMatch) {
406
+ steps.push({
407
+ clause: "WHERE",
408
+ sql: `WHERE ${whereMatch[1].trim()}`,
409
+ explanation: `Only deletes rows where: ${whereMatch[1].trim()}`,
410
+ });
411
+ } else {
412
+ steps.push({
413
+ clause: "WARNING",
414
+ sql: "No WHERE clause",
415
+ explanation: "WARNING: This deletes ALL rows from the table!",
416
+ });
417
+ }
418
+
419
+ return steps;
420
+ }
421
+
422
+ function explainCreateTable(sql: string): ExplanationStep[] {
423
+ const steps: ExplanationStep[] = [];
424
+ const norm = normalizeWhitespace(sql);
425
+
426
+ const tableMatch = norm.match(
427
+ /\bCREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(\w+(?:\.\w+)?)/i
428
+ );
429
+ if (tableMatch) {
430
+ const ifNotExists = /IF\s+NOT\s+EXISTS/i.test(norm) ? " (only if it doesn't already exist)" : "";
431
+ steps.push({
432
+ clause: "CREATE TABLE",
433
+ sql: `CREATE TABLE ${tableMatch[1]}`,
434
+ explanation: `Creates a new table called "${tableMatch[1]}"${ifNotExists}`,
435
+ });
436
+ }
437
+
438
+ // Extract column definitions
439
+ const bodyMatch = norm.match(/\([\s\S]+\)/);
440
+ if (bodyMatch) {
441
+ const body = bodyMatch[0].slice(1, -1);
442
+ const defs = splitRespectingParens(body);
443
+ const columns = defs.filter(
444
+ (d) => !/^\s*(PRIMARY KEY|FOREIGN KEY|UNIQUE|CHECK|CONSTRAINT)/i.test(d)
445
+ );
446
+ const constraints = defs.filter((d) =>
447
+ /^\s*(PRIMARY KEY|FOREIGN KEY|UNIQUE|CHECK|CONSTRAINT)/i.test(d)
448
+ );
449
+
450
+ if (columns.length > 0) {
451
+ steps.push({
452
+ clause: "COLUMNS",
453
+ sql: columns.map((c) => c.trim()).join(", "),
454
+ explanation: `Defines ${columns.length} column(s): ${columns.map((c) => c.trim().split(/\s+/)[0]).join(", ")}`,
455
+ });
456
+ }
457
+ if (constraints.length > 0) {
458
+ steps.push({
459
+ clause: "CONSTRAINTS",
460
+ sql: constraints.map((c) => c.trim()).join(", "),
461
+ explanation: `Adds ${constraints.length} table-level constraint(s)`,
462
+ });
463
+ }
464
+ }
465
+
466
+ return steps;
467
+ }
468
+
469
+ function generateTips(sql: string, queryType: string): string[] {
470
+ const tips: string[] = [];
471
+ const upper = normalizeWhitespace(sql).toUpperCase();
472
+
473
+ if (queryType === "SELECT") {
474
+ if (upper.includes("SELECT *")) {
475
+ tips.push("Consider selecting specific columns instead of * for better performance and clarity");
476
+ }
477
+ if (!upper.includes("LIMIT") && !upper.includes("FETCH")) {
478
+ tips.push("Consider adding a LIMIT clause to prevent returning too many rows");
479
+ }
480
+ if (upper.includes("LIKE '%") || upper.includes("LIKE \"%")) {
481
+ tips.push("Leading wildcard in LIKE pattern prevents index usage — consider full-text search");
482
+ }
483
+ if ((upper.match(/JOIN/g) || []).length > 3) {
484
+ tips.push("Multiple JOINs detected — ensure proper indexes exist on join columns");
485
+ }
486
+ }
487
+
488
+ if (queryType === "UPDATE" && !upper.includes("WHERE")) {
489
+ tips.push("No WHERE clause means ALL rows will be updated — double-check this is intended");
490
+ }
491
+
492
+ if (queryType === "DELETE" && !upper.includes("WHERE")) {
493
+ tips.push("No WHERE clause means ALL rows will be deleted — consider TRUNCATE if intentional");
494
+ }
495
+
496
+ if (/\bIN\s*\(\s*SELECT\b/i.test(upper)) {
497
+ tips.push("Subquery in IN clause — consider using EXISTS or a JOIN for better performance");
498
+ }
499
+
500
+ return tips;
501
+ }
502
+
503
+ export function sqlExplainer(sql: string): ExplanationResult {
504
+ const queryType = detectQueryType(sql);
505
+ const tables = extractTables(sql);
506
+ const columns = extractColumns(sql);
507
+ const conditions = extractConditions(sql);
508
+
509
+ let steps: ExplanationStep[];
510
+
511
+ switch (queryType) {
512
+ case "SELECT":
513
+ case "CTE (Common Table Expression)":
514
+ steps = explainSelect(sql);
515
+ break;
516
+ case "INSERT":
517
+ steps = explainInsert(sql);
518
+ break;
519
+ case "UPDATE":
520
+ steps = explainUpdate(sql);
521
+ break;
522
+ case "DELETE":
523
+ steps = explainDelete(sql);
524
+ break;
525
+ case "CREATE TABLE":
526
+ steps = explainCreateTable(sql);
527
+ break;
528
+ default:
529
+ steps = [
530
+ {
531
+ clause: queryType,
532
+ sql: normalizeWhitespace(sql).substring(0, 100),
533
+ explanation: `This is a ${queryType} statement`,
534
+ },
535
+ ];
536
+ }
537
+
538
+ const tips = generateTips(sql, queryType);
539
+
540
+ // Build summary
541
+ let summary: string;
542
+ switch (queryType) {
543
+ case "SELECT":
544
+ case "CTE (Common Table Expression)":
545
+ summary = `Queries data from ${tables.length > 0 ? tables.join(", ") : "unknown table(s)"}`;
546
+ if (conditions.length > 0) summary += ` with filtering conditions`;
547
+ break;
548
+ case "INSERT":
549
+ summary = `Inserts data into ${tables[0] || "a table"}`;
550
+ break;
551
+ case "UPDATE":
552
+ summary = `Updates data in ${tables[0] || "a table"}`;
553
+ break;
554
+ case "DELETE":
555
+ summary = `Deletes data from ${tables[0] || "a table"}`;
556
+ break;
557
+ case "CREATE TABLE":
558
+ summary = `Creates the "${tables[0] || "unknown"}" table`;
559
+ break;
560
+ default:
561
+ summary = `Executes a ${queryType} operation`;
562
+ }
563
+
564
+ return {
565
+ summary,
566
+ queryType,
567
+ steps,
568
+ tables,
569
+ columns,
570
+ conditions,
571
+ tips,
572
+ };
573
+ }