@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,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
+ }