@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,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Formatter — prettify SQL queries with proper indentation,
|
|
3
|
+
* uppercased keywords, and aligned columns.
|
|
4
|
+
* Supports MySQL, PostgreSQL, and SQLite syntax.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const SQL_KEYWORDS = [
|
|
8
|
+
"SELECT", "FROM", "WHERE", "AND", "OR", "NOT", "IN", "ON",
|
|
9
|
+
"JOIN", "INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN",
|
|
10
|
+
"LEFT OUTER JOIN", "RIGHT OUTER JOIN", "FULL OUTER JOIN", "CROSS JOIN",
|
|
11
|
+
"ORDER BY", "GROUP BY", "HAVING", "LIMIT", "OFFSET", "FETCH",
|
|
12
|
+
"INSERT INTO", "VALUES", "UPDATE", "SET", "DELETE FROM",
|
|
13
|
+
"CREATE TABLE", "ALTER TABLE", "DROP TABLE", "CREATE INDEX",
|
|
14
|
+
"DROP INDEX", "CREATE VIEW", "DROP VIEW",
|
|
15
|
+
"PRIMARY KEY", "FOREIGN KEY", "REFERENCES", "UNIQUE", "CHECK",
|
|
16
|
+
"DEFAULT", "NOT NULL", "AUTO_INCREMENT", "SERIAL", "BIGSERIAL",
|
|
17
|
+
"IF EXISTS", "IF NOT EXISTS", "CASCADE", "RESTRICT",
|
|
18
|
+
"UNION", "UNION ALL", "INTERSECT", "EXCEPT",
|
|
19
|
+
"EXISTS", "BETWEEN", "LIKE", "ILIKE", "IS NULL", "IS NOT NULL",
|
|
20
|
+
"ASC", "DESC", "DISTINCT", "ALL", "ANY", "SOME",
|
|
21
|
+
"CASE", "WHEN", "THEN", "ELSE", "END",
|
|
22
|
+
"AS", "ON", "USING", "INTO", "WITH", "RECURSIVE",
|
|
23
|
+
"RETURNING", "CONFLICT", "DO", "NOTHING",
|
|
24
|
+
"BEGIN", "COMMIT", "ROLLBACK", "SAVEPOINT",
|
|
25
|
+
"GRANT", "REVOKE", "TRUNCATE", "EXPLAIN", "ANALYZE",
|
|
26
|
+
"COALESCE", "CAST", "NULLIF", "GREATEST", "LEAST",
|
|
27
|
+
"COUNT", "SUM", "AVG", "MIN", "MAX",
|
|
28
|
+
"OVER", "PARTITION BY", "ROW_NUMBER", "RANK", "DENSE_RANK",
|
|
29
|
+
"CONSTRAINT", "ADD", "COLUMN", "RENAME", "TO",
|
|
30
|
+
"INTEGER", "INT", "BIGINT", "SMALLINT", "TINYINT",
|
|
31
|
+
"VARCHAR", "CHAR", "TEXT", "BOOLEAN", "BOOL",
|
|
32
|
+
"FLOAT", "DOUBLE", "DECIMAL", "NUMERIC", "REAL",
|
|
33
|
+
"DATE", "TIME", "TIMESTAMP", "DATETIME", "INTERVAL",
|
|
34
|
+
"JSON", "JSONB", "UUID", "BLOB", "BYTEA",
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
/** Major clause keywords that start a new line at indent level 0 */
|
|
38
|
+
const MAJOR_CLAUSES = [
|
|
39
|
+
"WITH", "SELECT", "FROM", "WHERE", "GROUP BY", "HAVING",
|
|
40
|
+
"ORDER BY", "LIMIT", "OFFSET", "FETCH",
|
|
41
|
+
"INSERT INTO", "VALUES", "UPDATE", "SET", "DELETE FROM",
|
|
42
|
+
"UNION", "UNION ALL", "INTERSECT", "EXCEPT",
|
|
43
|
+
"RETURNING", "ON CONFLICT",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/** Sub-clause keywords that start a new line indented */
|
|
47
|
+
const SUB_CLAUSES = [
|
|
48
|
+
"INNER JOIN", "LEFT JOIN", "RIGHT JOIN", "FULL JOIN",
|
|
49
|
+
"LEFT OUTER JOIN", "RIGHT OUTER JOIN", "FULL OUTER JOIN",
|
|
50
|
+
"CROSS JOIN", "JOIN", "ON", "AND", "OR",
|
|
51
|
+
"WHEN", "THEN", "ELSE", "END",
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
type Dialect = "mysql" | "postgresql" | "sqlite" | "generic";
|
|
55
|
+
|
|
56
|
+
interface FormatOptions {
|
|
57
|
+
dialect: Dialect;
|
|
58
|
+
indentSize: number;
|
|
59
|
+
uppercase: boolean;
|
|
60
|
+
alignColumns: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function uppercaseKeywords(sql: string): string {
|
|
64
|
+
// Sort keywords by length descending so longer multi-word keywords match first
|
|
65
|
+
const sorted = [...SQL_KEYWORDS].sort((a, b) => b.length - a.length);
|
|
66
|
+
|
|
67
|
+
let result = sql;
|
|
68
|
+
for (const kw of sorted) {
|
|
69
|
+
// Match keyword as a whole word, case-insensitive, but not inside quotes
|
|
70
|
+
const escaped = kw.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
71
|
+
const regex = new RegExp(`\\b${escaped}\\b`, "gi");
|
|
72
|
+
result = replaceOutsideStrings(result, regex, kw);
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function replaceOutsideStrings(
|
|
78
|
+
sql: string,
|
|
79
|
+
regex: RegExp,
|
|
80
|
+
replacement: string
|
|
81
|
+
): string {
|
|
82
|
+
const segments: string[] = [];
|
|
83
|
+
let inSingle = false;
|
|
84
|
+
let inDouble = false;
|
|
85
|
+
let current = "";
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < sql.length; i++) {
|
|
88
|
+
const ch = sql[i];
|
|
89
|
+
const prev = i > 0 ? sql[i - 1] : "";
|
|
90
|
+
|
|
91
|
+
if (ch === "'" && !inDouble && prev !== "\\") {
|
|
92
|
+
if (!inSingle) {
|
|
93
|
+
// Leaving non-string zone: process it
|
|
94
|
+
segments.push(current.replace(regex, replacement));
|
|
95
|
+
current = ch;
|
|
96
|
+
inSingle = true;
|
|
97
|
+
} else {
|
|
98
|
+
current += ch;
|
|
99
|
+
segments.push(current);
|
|
100
|
+
current = "";
|
|
101
|
+
inSingle = false;
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (ch === '"' && !inSingle && prev !== "\\") {
|
|
107
|
+
if (!inDouble) {
|
|
108
|
+
segments.push(current.replace(regex, replacement));
|
|
109
|
+
current = ch;
|
|
110
|
+
inDouble = true;
|
|
111
|
+
} else {
|
|
112
|
+
current += ch;
|
|
113
|
+
segments.push(current);
|
|
114
|
+
current = "";
|
|
115
|
+
inDouble = false;
|
|
116
|
+
}
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
current += ch;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (current) {
|
|
124
|
+
if (inSingle || inDouble) {
|
|
125
|
+
segments.push(current);
|
|
126
|
+
} else {
|
|
127
|
+
segments.push(current.replace(regex, replacement));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return segments.join("");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function formatSql(sql: string, options: FormatOptions): string {
|
|
135
|
+
// Normalize whitespace
|
|
136
|
+
let formatted = sql.replace(/\s+/g, " ").trim();
|
|
137
|
+
|
|
138
|
+
// Uppercase keywords
|
|
139
|
+
if (options.uppercase) {
|
|
140
|
+
formatted = uppercaseKeywords(formatted);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const indent = " ".repeat(options.indentSize);
|
|
144
|
+
|
|
145
|
+
// Insert newlines before major clauses
|
|
146
|
+
for (const clause of MAJOR_CLAUSES) {
|
|
147
|
+
const escaped = clause.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
148
|
+
const regex = new RegExp(`\\s+${escaped}\\b`, "g");
|
|
149
|
+
formatted = formatted.replace(regex, `\n${clause}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Insert newlines + indent before sub-clauses
|
|
153
|
+
for (const sub of SUB_CLAUSES) {
|
|
154
|
+
const escaped = sub.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
155
|
+
// Don't add newline before ON when it's part of ON CONFLICT
|
|
156
|
+
if (sub === "ON") {
|
|
157
|
+
const regex = new RegExp(`\\s+ON\\b(?!\\s+CONFLICT)`, "g");
|
|
158
|
+
formatted = formatted.replace(regex, `\n${indent}${sub}`);
|
|
159
|
+
} else {
|
|
160
|
+
const regex = new RegExp(`\\s+${escaped}\\b`, "g");
|
|
161
|
+
formatted = formatted.replace(regex, `\n${indent}${sub}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Align SELECT columns: put each column on its own line
|
|
166
|
+
if (options.alignColumns) {
|
|
167
|
+
formatted = formatted.replace(
|
|
168
|
+
/SELECT\s+([\s\S]*?)(?=\nFROM)/gi,
|
|
169
|
+
(_match, cols: string) => {
|
|
170
|
+
const columns = splitColumns(cols.trim());
|
|
171
|
+
if (columns.length > 1) {
|
|
172
|
+
return (
|
|
173
|
+
"SELECT\n" +
|
|
174
|
+
columns.map((c) => `${indent}${c.trim()}`).join(",\n")
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return `SELECT ${cols.trim()}`;
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Clean up multiple blank lines
|
|
183
|
+
formatted = formatted.replace(/\n{3,}/g, "\n\n");
|
|
184
|
+
|
|
185
|
+
return formatted.trim();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** Split columns respecting parentheses depth */
|
|
189
|
+
function splitColumns(cols: string): string[] {
|
|
190
|
+
const result: string[] = [];
|
|
191
|
+
let depth = 0;
|
|
192
|
+
let current = "";
|
|
193
|
+
|
|
194
|
+
for (const ch of cols) {
|
|
195
|
+
if (ch === "(") depth++;
|
|
196
|
+
if (ch === ")") depth--;
|
|
197
|
+
if (ch === "," && depth === 0) {
|
|
198
|
+
result.push(current.trim());
|
|
199
|
+
current = "";
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
current += ch;
|
|
203
|
+
}
|
|
204
|
+
if (current.trim()) result.push(current.trim());
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function sqlFormatter(
|
|
209
|
+
sql: string,
|
|
210
|
+
dialect: string = "generic",
|
|
211
|
+
indentSize: number = 2,
|
|
212
|
+
uppercase: boolean = true,
|
|
213
|
+
alignColumns: boolean = true
|
|
214
|
+
): string {
|
|
215
|
+
const d = (["mysql", "postgresql", "sqlite"].includes(dialect)
|
|
216
|
+
? dialect
|
|
217
|
+
: "generic") as Dialect;
|
|
218
|
+
|
|
219
|
+
const options: FormatOptions = {
|
|
220
|
+
dialect: d,
|
|
221
|
+
indentSize,
|
|
222
|
+
uppercase,
|
|
223
|
+
alignColumns,
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// Handle multiple statements separated by semicolons
|
|
227
|
+
const statements = sql
|
|
228
|
+
.split(";")
|
|
229
|
+
.map((s) => s.trim())
|
|
230
|
+
.filter(Boolean);
|
|
231
|
+
|
|
232
|
+
return statements.map((stmt) => formatSql(stmt, options)).join(";\n\n") + ";";
|
|
233
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"]
|
|
14
|
+
}
|