@zlash65/postgres-ssh-mcp 0.0.1
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/LICENSE +21 -0
- package/README.md +161 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +122 -0
- package/dist/config.js.map +1 -0
- package/dist/connection/host-key-verifier.d.ts +13 -0
- package/dist/connection/host-key-verifier.d.ts.map +1 -0
- package/dist/connection/host-key-verifier.js +127 -0
- package/dist/connection/host-key-verifier.js.map +1 -0
- package/dist/connection/index.d.ts +7 -0
- package/dist/connection/index.d.ts.map +1 -0
- package/dist/connection/index.js +7 -0
- package/dist/connection/index.js.map +1 -0
- package/dist/connection/postgres-pool.d.ts +23 -0
- package/dist/connection/postgres-pool.d.ts.map +1 -0
- package/dist/connection/postgres-pool.js +295 -0
- package/dist/connection/postgres-pool.js.map +1 -0
- package/dist/connection/ssh-tunnel.d.ts +34 -0
- package/dist/connection/ssh-tunnel.d.ts.map +1 -0
- package/dist/connection/ssh-tunnel.js +295 -0
- package/dist/connection/ssh-tunnel.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +67 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/obfuscate.d.ts +2 -0
- package/dist/lib/obfuscate.d.ts.map +1 -0
- package/dist/lib/obfuscate.js +13 -0
- package/dist/lib/obfuscate.js.map +1 -0
- package/dist/lib/sql-validator.d.ts +6 -0
- package/dist/lib/sql-validator.d.ts.map +1 -0
- package/dist/lib/sql-validator.js +684 -0
- package/dist/lib/sql-validator.js.map +1 -0
- package/dist/lib/tool-response.d.ts +5 -0
- package/dist/lib/tool-response.d.ts.map +1 -0
- package/dist/lib/tool-response.js +30 -0
- package/dist/lib/tool-response.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +24 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/admin.d.ts +4 -0
- package/dist/tools/admin.d.ts.map +1 -0
- package/dist/tools/admin.js +184 -0
- package/dist/tools/admin.js.map +1 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +8 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/query.d.ts +4 -0
- package/dist/tools/query.d.ts.map +1 -0
- package/dist/tools/query.js +65 -0
- package/dist/tools/query.js.map +1 -0
- package/dist/tools/schema.d.ts +4 -0
- package/dist/tools/schema.d.ts.map +1 -0
- package/dist/tools/schema.js +189 -0
- package/dist/tools/schema.js.map +1 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,684 @@
|
|
|
1
|
+
export function validateReadOnlyStatement(sql) {
|
|
2
|
+
if (containsMultipleStatements(sql)) {
|
|
3
|
+
throw new Error('Multiple statements not allowed. Submit one statement at a time.');
|
|
4
|
+
}
|
|
5
|
+
const strippedSql = stripLeadingComments(sql);
|
|
6
|
+
const normalized = strippedSql.trim().toUpperCase();
|
|
7
|
+
const firstKeyword = getFirstKeyword(strippedSql);
|
|
8
|
+
if (!firstKeyword) {
|
|
9
|
+
throw new Error('Empty SQL statement.');
|
|
10
|
+
}
|
|
11
|
+
if (firstKeyword === 'CALL') {
|
|
12
|
+
throw new Error('CALL statements not allowed in read-only mode (procedures may modify data).');
|
|
13
|
+
}
|
|
14
|
+
if (firstKeyword === 'DO') {
|
|
15
|
+
throw new Error('DO statements not allowed in read-only mode (anonymous blocks may modify data).');
|
|
16
|
+
}
|
|
17
|
+
if (firstKeyword === 'SELECT') {
|
|
18
|
+
if (containsSelectInto(strippedSql)) {
|
|
19
|
+
throw new Error('SELECT INTO not allowed in read-only mode (creates tables). Use SELECT without INTO.');
|
|
20
|
+
}
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (firstKeyword === 'EXPLAIN') {
|
|
24
|
+
validateExplainStatement(strippedSql);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (firstKeyword === 'SHOW') {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (firstKeyword === 'VALUES') {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (firstKeyword === 'TABLE') {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (firstKeyword === 'WITH') {
|
|
37
|
+
if (validateWithStatement(strippedSql)) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
throw new Error('WITH statements only allowed when final statement is SELECT. ' +
|
|
41
|
+
'WITH ... INSERT/UPDATE/DELETE/MERGE not permitted in read-only mode.');
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Statement type not allowed in read-only mode. ` +
|
|
44
|
+
`Allowed: SELECT, EXPLAIN (without ANALYZE on DML), SHOW, VALUES, TABLE, WITH...SELECT. ` +
|
|
45
|
+
`Received: ${normalized.slice(0, 50)}...`);
|
|
46
|
+
}
|
|
47
|
+
function containsSelectInto(sql) {
|
|
48
|
+
const tokens = tokenizeSimple(sql);
|
|
49
|
+
let depth = 0;
|
|
50
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
51
|
+
const token = tokens[i].toUpperCase();
|
|
52
|
+
if (token === '(')
|
|
53
|
+
depth++;
|
|
54
|
+
else if (token === ')')
|
|
55
|
+
depth--;
|
|
56
|
+
if (token === 'INTO' && depth === 0) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
function validateExplainStatement(sql) {
|
|
63
|
+
const normalized = stripLeadingComments(sql).trim().toUpperCase();
|
|
64
|
+
let remaining = normalized.slice('EXPLAIN'.length);
|
|
65
|
+
remaining = stripLeadingComments(remaining).trim();
|
|
66
|
+
let hasAnalyze = false;
|
|
67
|
+
if (remaining.startsWith('(')) {
|
|
68
|
+
const closeParenIndex = remaining.indexOf(')');
|
|
69
|
+
if (closeParenIndex > 0) {
|
|
70
|
+
const options = remaining.slice(1, closeParenIndex).toUpperCase();
|
|
71
|
+
hasAnalyze = options.includes('ANALYZE');
|
|
72
|
+
remaining = remaining.slice(closeParenIndex + 1);
|
|
73
|
+
remaining = stripLeadingComments(remaining).trim();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (remaining.startsWith('ANALYZE ') ||
|
|
77
|
+
remaining.startsWith('ANALYZE\n') ||
|
|
78
|
+
remaining.startsWith('ANALYZE\t')) {
|
|
79
|
+
hasAnalyze = true;
|
|
80
|
+
remaining = remaining.slice('ANALYZE'.length);
|
|
81
|
+
remaining = stripLeadingComments(remaining).trim();
|
|
82
|
+
}
|
|
83
|
+
const skipKeywords = [
|
|
84
|
+
'VERBOSE',
|
|
85
|
+
'COSTS',
|
|
86
|
+
'SETTINGS',
|
|
87
|
+
'BUFFERS',
|
|
88
|
+
'WAL',
|
|
89
|
+
'TIMING',
|
|
90
|
+
'SUMMARY',
|
|
91
|
+
'FORMAT',
|
|
92
|
+
];
|
|
93
|
+
let changed = true;
|
|
94
|
+
while (changed) {
|
|
95
|
+
changed = false;
|
|
96
|
+
remaining = stripLeadingComments(remaining).trim();
|
|
97
|
+
for (const kw of skipKeywords) {
|
|
98
|
+
if (remaining.startsWith(kw + ' ') ||
|
|
99
|
+
remaining.startsWith(kw + '\n') ||
|
|
100
|
+
remaining.startsWith(kw + '\t')) {
|
|
101
|
+
remaining = remaining.slice(kw.length);
|
|
102
|
+
remaining = stripLeadingComments(remaining).trim();
|
|
103
|
+
changed = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const formatTypes = ['TEXT', 'JSON', 'XML', 'YAML'];
|
|
107
|
+
for (const ft of formatTypes) {
|
|
108
|
+
if (remaining.startsWith(ft + ' ') ||
|
|
109
|
+
remaining.startsWith(ft + '\n') ||
|
|
110
|
+
remaining.startsWith(ft + '\t') ||
|
|
111
|
+
remaining.startsWith(ft + ')')) {
|
|
112
|
+
remaining = remaining.slice(ft.length);
|
|
113
|
+
remaining = stripLeadingComments(remaining).trim();
|
|
114
|
+
changed = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const innerStatement = remaining;
|
|
119
|
+
if (hasAnalyze) {
|
|
120
|
+
const normalizedInner = stripLeadingComments(innerStatement).trim().toUpperCase();
|
|
121
|
+
const safeForAnalyze = normalizedInner.startsWith('SELECT ') ||
|
|
122
|
+
normalizedInner.startsWith('SELECT\n') ||
|
|
123
|
+
normalizedInner.startsWith('SELECT\t') ||
|
|
124
|
+
normalizedInner.startsWith('TABLE ') ||
|
|
125
|
+
normalizedInner.startsWith('VALUES ') ||
|
|
126
|
+
normalizedInner.startsWith('VALUES(') ||
|
|
127
|
+
normalizedInner.startsWith('WITH ') ||
|
|
128
|
+
normalizedInner.startsWith('WITH\n');
|
|
129
|
+
if (!safeForAnalyze) {
|
|
130
|
+
throw new Error('EXPLAIN ANALYZE not allowed for INSERT/UPDATE/DELETE/MERGE in read-only mode. ' +
|
|
131
|
+
'ANALYZE actually executes the statement! Use EXPLAIN without ANALYZE.');
|
|
132
|
+
}
|
|
133
|
+
if (normalizedInner.startsWith('WITH ') || normalizedInner.startsWith('WITH\n')) {
|
|
134
|
+
const finalStatement = extractFinalStatementAfterCTEs(innerStatement);
|
|
135
|
+
if (finalStatement === null) {
|
|
136
|
+
throw new Error('EXPLAIN ANALYZE WITH ... must end with SELECT. ' +
|
|
137
|
+
'Could not parse CTE structure.');
|
|
138
|
+
}
|
|
139
|
+
const normalizedFinal = stripLeadingComments(finalStatement).trim().toUpperCase();
|
|
140
|
+
const isSafeFinalStatement = normalizedFinal.startsWith('SELECT ') ||
|
|
141
|
+
normalizedFinal.startsWith('SELECT\n') ||
|
|
142
|
+
normalizedFinal.startsWith('SELECT\t') ||
|
|
143
|
+
normalizedFinal.startsWith('TABLE ') ||
|
|
144
|
+
normalizedFinal.startsWith('VALUES ') ||
|
|
145
|
+
normalizedFinal.startsWith('VALUES(');
|
|
146
|
+
if (!isSafeFinalStatement) {
|
|
147
|
+
throw new Error('EXPLAIN ANALYZE WITH ... must end with SELECT. ' +
|
|
148
|
+
'ANALYZE on WITH...INSERT/UPDATE/DELETE will execute the statement! ' +
|
|
149
|
+
`Found final statement starting with: ${normalizedFinal.slice(0, 30)}...`);
|
|
150
|
+
}
|
|
151
|
+
if (cteContainsDML(innerStatement)) {
|
|
152
|
+
throw new Error('EXPLAIN ANALYZE not allowed on data-modifying CTEs. ' +
|
|
153
|
+
'CTEs containing INSERT/UPDATE/DELETE will execute when using ANALYZE.');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const blocked = ['CALL ', 'DO '];
|
|
158
|
+
for (const prefix of blocked) {
|
|
159
|
+
if (innerStatement.startsWith(prefix)) {
|
|
160
|
+
throw new Error(`EXPLAIN of ${prefix.trim()} not allowed in read-only mode.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function validateWithStatement(sql) {
|
|
165
|
+
if (cteContainsDML(sql)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
const finalStatement = extractFinalStatementAfterCTEs(sql);
|
|
169
|
+
if (!finalStatement)
|
|
170
|
+
return false;
|
|
171
|
+
const normalized = stripLeadingComments(finalStatement).trim().toUpperCase();
|
|
172
|
+
const isAllowed = normalized.startsWith('SELECT ') ||
|
|
173
|
+
normalized.startsWith('SELECT\n') ||
|
|
174
|
+
normalized.startsWith('SELECT\t') ||
|
|
175
|
+
normalized.startsWith('TABLE ') ||
|
|
176
|
+
normalized.startsWith('VALUES ') ||
|
|
177
|
+
normalized.startsWith('VALUES(');
|
|
178
|
+
if (!isAllowed) {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
if (normalized.startsWith('SELECT')) {
|
|
182
|
+
return !containsSelectInto(finalStatement);
|
|
183
|
+
}
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
function skipWhitespaceAndComments(sql, start) {
|
|
187
|
+
let i = start;
|
|
188
|
+
while (i < sql.length) {
|
|
189
|
+
const char = sql[i];
|
|
190
|
+
const nextChar = sql[i + 1] || '';
|
|
191
|
+
if (/\s/.test(char)) {
|
|
192
|
+
i++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (char === '-' && nextChar === '-') {
|
|
196
|
+
i += 2;
|
|
197
|
+
while (i < sql.length && sql[i] !== '\n') {
|
|
198
|
+
i++;
|
|
199
|
+
}
|
|
200
|
+
if (i < sql.length)
|
|
201
|
+
i++;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (char === '/' && nextChar === '*') {
|
|
205
|
+
i += 2;
|
|
206
|
+
while (i < sql.length - 1) {
|
|
207
|
+
if (sql[i] === '*' && sql[i + 1] === '/') {
|
|
208
|
+
i += 2;
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
i++;
|
|
212
|
+
}
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
return i;
|
|
218
|
+
}
|
|
219
|
+
export function cteContainsDML(sql) {
|
|
220
|
+
const upperSql = sql.toUpperCase();
|
|
221
|
+
let searchStart = 0;
|
|
222
|
+
while (true) {
|
|
223
|
+
const asIndex = upperSql.indexOf('AS', searchStart);
|
|
224
|
+
if (asIndex === -1)
|
|
225
|
+
break;
|
|
226
|
+
const charBefore = asIndex > 0 ? upperSql[asIndex - 1] : ' ';
|
|
227
|
+
const charAfter = upperSql[asIndex + 2] || ' ';
|
|
228
|
+
if (/[A-Z0-9_]/.test(charBefore) || /[A-Z0-9_]/.test(charAfter)) {
|
|
229
|
+
searchStart = asIndex + 2;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const parenStart = skipWhitespaceAndComments(sql, asIndex + 2);
|
|
233
|
+
if (parenStart >= sql.length || sql[parenStart] !== '(') {
|
|
234
|
+
searchStart = parenStart > asIndex + 2 ? parenStart : asIndex + 2;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const cteBody = extractParenthesizedContent(sql, parenStart);
|
|
238
|
+
if (cteBody === null) {
|
|
239
|
+
searchStart = parenStart + 1;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const strippedBody = stripLeadingComments(cteBody).trim().toUpperCase();
|
|
243
|
+
const tokens = tokenizeSimple(strippedBody);
|
|
244
|
+
for (const token of tokens) {
|
|
245
|
+
const upper = token.toUpperCase();
|
|
246
|
+
if (upper === 'INSERT' ||
|
|
247
|
+
upper === 'UPDATE' ||
|
|
248
|
+
upper === 'DELETE' ||
|
|
249
|
+
upper === 'MERGE') {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
searchStart = parenStart + cteBody.length + 2;
|
|
254
|
+
}
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
function extractParenthesizedContent(sql, openParenIndex) {
|
|
258
|
+
if (sql[openParenIndex] !== '(')
|
|
259
|
+
return null;
|
|
260
|
+
let depth = 1;
|
|
261
|
+
let i = openParenIndex + 1;
|
|
262
|
+
let inSingleQuote = false;
|
|
263
|
+
let inDoubleQuote = false;
|
|
264
|
+
let inDollarQuote = false;
|
|
265
|
+
let dollarTag = '';
|
|
266
|
+
let inLineComment = false;
|
|
267
|
+
let inBlockComment = false;
|
|
268
|
+
while (i < sql.length && depth > 0) {
|
|
269
|
+
const char = sql[i];
|
|
270
|
+
const nextChar = sql[i + 1] || '';
|
|
271
|
+
if (!inSingleQuote && !inDoubleQuote && !inDollarQuote && !inBlockComment) {
|
|
272
|
+
if (char === '-' && nextChar === '-') {
|
|
273
|
+
inLineComment = true;
|
|
274
|
+
i += 2;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (inLineComment) {
|
|
279
|
+
if (char === '\n')
|
|
280
|
+
inLineComment = false;
|
|
281
|
+
i++;
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (!inSingleQuote && !inDoubleQuote && !inDollarQuote) {
|
|
285
|
+
if (char === '/' && nextChar === '*') {
|
|
286
|
+
inBlockComment = true;
|
|
287
|
+
i += 2;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (inBlockComment) {
|
|
292
|
+
if (char === '*' && nextChar === '/') {
|
|
293
|
+
inBlockComment = false;
|
|
294
|
+
i += 2;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
i++;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (char === '$' && !inSingleQuote && !inDoubleQuote) {
|
|
301
|
+
if (!inDollarQuote) {
|
|
302
|
+
const tagMatch = sql.slice(i).match(/^\$([a-zA-Z0-9_]*)\$/);
|
|
303
|
+
if (tagMatch) {
|
|
304
|
+
dollarTag = tagMatch[0];
|
|
305
|
+
inDollarQuote = true;
|
|
306
|
+
i += dollarTag.length;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else if (sql.slice(i, i + dollarTag.length) === dollarTag) {
|
|
311
|
+
inDollarQuote = false;
|
|
312
|
+
i += dollarTag.length;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (inDollarQuote) {
|
|
317
|
+
i++;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (char === "'" && !inDoubleQuote) {
|
|
321
|
+
if (inSingleQuote && nextChar === "'") {
|
|
322
|
+
i += 2;
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
inSingleQuote = !inSingleQuote;
|
|
326
|
+
i++;
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (char === '"' && !inSingleQuote) {
|
|
330
|
+
if (inDoubleQuote && nextChar === '"') {
|
|
331
|
+
i += 2;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
inDoubleQuote = !inDoubleQuote;
|
|
335
|
+
i++;
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (inSingleQuote || inDoubleQuote) {
|
|
339
|
+
i++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (char === '(')
|
|
343
|
+
depth++;
|
|
344
|
+
else if (char === ')')
|
|
345
|
+
depth--;
|
|
346
|
+
i++;
|
|
347
|
+
}
|
|
348
|
+
if (depth !== 0)
|
|
349
|
+
return null;
|
|
350
|
+
return sql.slice(openParenIndex + 1, i - 1);
|
|
351
|
+
}
|
|
352
|
+
export function extractFinalStatementAfterCTEs(sql) {
|
|
353
|
+
let depth = 0;
|
|
354
|
+
let lastCTEEnd = -1;
|
|
355
|
+
let inSingleQuote = false;
|
|
356
|
+
let inDoubleQuote = false;
|
|
357
|
+
let inDollarQuote = false;
|
|
358
|
+
let inLineComment = false;
|
|
359
|
+
let inBlockComment = false;
|
|
360
|
+
let dollarTag = '';
|
|
361
|
+
const upperSql = sql.toUpperCase();
|
|
362
|
+
let startIdx = upperSql.indexOf('WITH');
|
|
363
|
+
if (startIdx === -1)
|
|
364
|
+
return null;
|
|
365
|
+
startIdx += 4;
|
|
366
|
+
for (let i = startIdx; i < sql.length; i++) {
|
|
367
|
+
const char = sql[i];
|
|
368
|
+
const nextChar = sql[i + 1] || '';
|
|
369
|
+
if (!inSingleQuote && !inDoubleQuote && !inDollarQuote && !inBlockComment) {
|
|
370
|
+
if (char === '-' && nextChar === '-') {
|
|
371
|
+
inLineComment = true;
|
|
372
|
+
i++;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (inLineComment) {
|
|
377
|
+
if (char === '\n')
|
|
378
|
+
inLineComment = false;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (!inSingleQuote && !inDoubleQuote && !inDollarQuote) {
|
|
382
|
+
if (char === '/' && nextChar === '*') {
|
|
383
|
+
inBlockComment = true;
|
|
384
|
+
i++;
|
|
385
|
+
continue;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (inBlockComment) {
|
|
389
|
+
if (char === '*' && nextChar === '/') {
|
|
390
|
+
inBlockComment = false;
|
|
391
|
+
i++;
|
|
392
|
+
}
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (char === '$' && !inSingleQuote && !inDoubleQuote && !inDollarQuote) {
|
|
396
|
+
const tagMatch = sql.slice(i).match(/^\$([a-zA-Z0-9_]*)\$/);
|
|
397
|
+
if (tagMatch) {
|
|
398
|
+
dollarTag = tagMatch[0];
|
|
399
|
+
inDollarQuote = true;
|
|
400
|
+
i += dollarTag.length - 1;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else if (inDollarQuote && sql.slice(i, i + dollarTag.length) === dollarTag) {
|
|
405
|
+
inDollarQuote = false;
|
|
406
|
+
i += dollarTag.length - 1;
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (inDollarQuote)
|
|
410
|
+
continue;
|
|
411
|
+
if (char === "'" && !inDoubleQuote) {
|
|
412
|
+
if (inSingleQuote && nextChar === "'") {
|
|
413
|
+
i++;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
inSingleQuote = !inSingleQuote;
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
if (char === '"' && !inSingleQuote) {
|
|
420
|
+
if (inDoubleQuote && nextChar === '"') {
|
|
421
|
+
i++;
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
inDoubleQuote = !inDoubleQuote;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
if (inSingleQuote || inDoubleQuote)
|
|
428
|
+
continue;
|
|
429
|
+
if (char === '(') {
|
|
430
|
+
depth++;
|
|
431
|
+
}
|
|
432
|
+
else if (char === ')') {
|
|
433
|
+
depth--;
|
|
434
|
+
if (depth === 0) {
|
|
435
|
+
lastCTEEnd = i;
|
|
436
|
+
let j = i + 1;
|
|
437
|
+
while (j < sql.length && /\s/.test(sql[j]))
|
|
438
|
+
j++;
|
|
439
|
+
while (j < sql.length) {
|
|
440
|
+
if (sql.slice(j, j + 2) === '--') {
|
|
441
|
+
const nl = sql.indexOf('\n', j);
|
|
442
|
+
j = nl === -1 ? sql.length : nl + 1;
|
|
443
|
+
while (j < sql.length && /\s/.test(sql[j]))
|
|
444
|
+
j++;
|
|
445
|
+
}
|
|
446
|
+
else if (sql.slice(j, j + 2) === '/*') {
|
|
447
|
+
const end = sql.indexOf('*/', j);
|
|
448
|
+
j = end === -1 ? sql.length : end + 2;
|
|
449
|
+
while (j < sql.length && /\s/.test(sql[j]))
|
|
450
|
+
j++;
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
break;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (j < sql.length && sql[j] !== ',') {
|
|
457
|
+
return sql.slice(j);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (lastCTEEnd !== -1) {
|
|
463
|
+
return sql.slice(lastCTEEnd + 1);
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
export function stripLeadingComments(sql) {
|
|
468
|
+
let result = sql.trimStart();
|
|
469
|
+
while (true) {
|
|
470
|
+
if (result.startsWith('--')) {
|
|
471
|
+
const newlineIndex = result.indexOf('\n');
|
|
472
|
+
if (newlineIndex === -1)
|
|
473
|
+
return '';
|
|
474
|
+
result = result.slice(newlineIndex + 1).trimStart();
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (result.startsWith('/*')) {
|
|
478
|
+
const endIndex = result.indexOf('*/');
|
|
479
|
+
if (endIndex === -1)
|
|
480
|
+
return '';
|
|
481
|
+
result = result.slice(endIndex + 2).trimStart();
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
return result;
|
|
487
|
+
}
|
|
488
|
+
function containsMultipleStatements(sql) {
|
|
489
|
+
let inSingleQuote = false;
|
|
490
|
+
let inDoubleQuote = false;
|
|
491
|
+
let inDollarQuote = false;
|
|
492
|
+
let inLineComment = false;
|
|
493
|
+
let inBlockComment = false;
|
|
494
|
+
let dollarTag = '';
|
|
495
|
+
let semicolonCount = 0;
|
|
496
|
+
let lastSemicolonIndex = -1;
|
|
497
|
+
for (let i = 0; i < sql.length; i++) {
|
|
498
|
+
const char = sql[i];
|
|
499
|
+
const nextChar = sql[i + 1] || '';
|
|
500
|
+
if (!inSingleQuote && !inDoubleQuote && !inDollarQuote && !inBlockComment) {
|
|
501
|
+
if (char === '-' && nextChar === '-') {
|
|
502
|
+
inLineComment = true;
|
|
503
|
+
i++;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (inLineComment) {
|
|
508
|
+
if (char === '\n')
|
|
509
|
+
inLineComment = false;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
if (!inSingleQuote && !inDoubleQuote && !inDollarQuote && !inLineComment) {
|
|
513
|
+
if (char === '/' && nextChar === '*') {
|
|
514
|
+
inBlockComment = true;
|
|
515
|
+
i++;
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (inBlockComment) {
|
|
520
|
+
if (char === '*' && nextChar === '/') {
|
|
521
|
+
inBlockComment = false;
|
|
522
|
+
i++;
|
|
523
|
+
}
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
if (char === '$' && !inSingleQuote && !inDoubleQuote) {
|
|
527
|
+
if (!inDollarQuote) {
|
|
528
|
+
const tagMatch = sql.slice(i).match(/^\$([a-zA-Z0-9_]*)\$/);
|
|
529
|
+
if (tagMatch) {
|
|
530
|
+
dollarTag = tagMatch[0];
|
|
531
|
+
inDollarQuote = true;
|
|
532
|
+
i += dollarTag.length - 1;
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else if (sql.slice(i, i + dollarTag.length) === dollarTag) {
|
|
537
|
+
inDollarQuote = false;
|
|
538
|
+
i += dollarTag.length - 1;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (inDollarQuote)
|
|
543
|
+
continue;
|
|
544
|
+
if (char === "'" && !inDoubleQuote) {
|
|
545
|
+
if (inSingleQuote && nextChar === "'") {
|
|
546
|
+
i++;
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
inSingleQuote = !inSingleQuote;
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (char === '"' && !inSingleQuote) {
|
|
553
|
+
if (inDoubleQuote && nextChar === '"') {
|
|
554
|
+
i++;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
inDoubleQuote = !inDoubleQuote;
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (inSingleQuote || inDoubleQuote)
|
|
561
|
+
continue;
|
|
562
|
+
if (char === ';') {
|
|
563
|
+
semicolonCount++;
|
|
564
|
+
lastSemicolonIndex = i;
|
|
565
|
+
if (semicolonCount > 1)
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
if (lastSemicolonIndex > -1) {
|
|
570
|
+
const afterSemicolon = stripLeadingComments(sql.slice(lastSemicolonIndex + 1)).trim();
|
|
571
|
+
if (afterSemicolon.length > 0) {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
function tokenizeSimple(sql) {
|
|
578
|
+
const tokens = [];
|
|
579
|
+
let current = '';
|
|
580
|
+
let inString = false;
|
|
581
|
+
let stringChar = '';
|
|
582
|
+
let inDollarQuote = false;
|
|
583
|
+
let dollarTag = '';
|
|
584
|
+
let inLineComment = false;
|
|
585
|
+
let inBlockComment = false;
|
|
586
|
+
for (let i = 0; i < sql.length; i++) {
|
|
587
|
+
const char = sql[i];
|
|
588
|
+
const nextChar = sql[i + 1] || '';
|
|
589
|
+
if (!inString && !inDollarQuote && !inBlockComment && char === '-' && nextChar === '-') {
|
|
590
|
+
if (current)
|
|
591
|
+
tokens.push(current);
|
|
592
|
+
current = '';
|
|
593
|
+
inLineComment = true;
|
|
594
|
+
i++;
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
if (inLineComment) {
|
|
598
|
+
if (char === '\n')
|
|
599
|
+
inLineComment = false;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (!inString && !inDollarQuote && !inLineComment && char === '/' && nextChar === '*') {
|
|
603
|
+
if (current)
|
|
604
|
+
tokens.push(current);
|
|
605
|
+
current = '';
|
|
606
|
+
inBlockComment = true;
|
|
607
|
+
i++;
|
|
608
|
+
continue;
|
|
609
|
+
}
|
|
610
|
+
if (inBlockComment) {
|
|
611
|
+
if (char === '*' && nextChar === '/') {
|
|
612
|
+
inBlockComment = false;
|
|
613
|
+
i++;
|
|
614
|
+
}
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (char === '$' && !inString) {
|
|
618
|
+
if (!inDollarQuote) {
|
|
619
|
+
const match = sql.slice(i).match(/^\$([a-zA-Z0-9_]*)\$/);
|
|
620
|
+
if (match) {
|
|
621
|
+
if (current)
|
|
622
|
+
tokens.push(current);
|
|
623
|
+
current = '';
|
|
624
|
+
dollarTag = match[0];
|
|
625
|
+
inDollarQuote = true;
|
|
626
|
+
i += dollarTag.length - 1;
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
else if (sql.slice(i, i + dollarTag.length) === dollarTag) {
|
|
631
|
+
inDollarQuote = false;
|
|
632
|
+
i += dollarTag.length - 1;
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (inDollarQuote)
|
|
637
|
+
continue;
|
|
638
|
+
if ((char === "'" || char === '"') && !inString) {
|
|
639
|
+
if (current)
|
|
640
|
+
tokens.push(current);
|
|
641
|
+
current = '';
|
|
642
|
+
inString = true;
|
|
643
|
+
stringChar = char;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
if (inString && char === stringChar) {
|
|
647
|
+
if (sql[i + 1] === stringChar) {
|
|
648
|
+
i++;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
inString = false;
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
if (inString)
|
|
655
|
+
continue;
|
|
656
|
+
if (/\s/.test(char)) {
|
|
657
|
+
if (current)
|
|
658
|
+
tokens.push(current);
|
|
659
|
+
current = '';
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
if ('(),;'.includes(char)) {
|
|
663
|
+
if (current)
|
|
664
|
+
tokens.push(current);
|
|
665
|
+
tokens.push(char);
|
|
666
|
+
current = '';
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
current += char;
|
|
670
|
+
}
|
|
671
|
+
if (current)
|
|
672
|
+
tokens.push(current);
|
|
673
|
+
return tokens;
|
|
674
|
+
}
|
|
675
|
+
export function getFirstKeyword(sql) {
|
|
676
|
+
const tokens = tokenizeSimple(sql);
|
|
677
|
+
for (const token of tokens) {
|
|
678
|
+
if (/^[A-Za-z_]/.test(token)) {
|
|
679
|
+
return token.toUpperCase();
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return null;
|
|
683
|
+
}
|
|
684
|
+
//# sourceMappingURL=sql-validator.js.map
|