@yejiming/dsh-data-agent 0.0.6 → 0.0.10
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.en.md +142 -119
- package/README.md +138 -118
- package/cordis.patch.yml +8 -9
- package/lib/client.js +42423 -524
- package/lib/client.js.map +1 -1
- package/lib/command-LFgLb6el.js +875 -0
- package/lib/command.js +2 -0
- package/lib/connections-WmjuUrDj.js +1608 -0
- package/lib/defaults-DP4RyRh1.js +21 -0
- package/lib/index.js +265 -68
- package/lib/routes.js +94 -170
- package/lib/tool-Dka6RyEp.js +1128 -0
- package/lib/tool.js +1 -426
- package/lib/types/analysis.d.ts +1071 -0
- package/lib/types/client/AnalysisChart.d.ts +26 -0
- package/lib/types/client/AnalysisDashboard.d.ts +30 -0
- package/lib/types/client/DataAgentWorkbench.d.ts +2 -2
- package/lib/types/client/analysis-charts.d.ts +40 -0
- package/lib/types/client/analysis-view-model.d.ts +44 -0
- package/lib/types/client/index.d.ts +3 -4
- package/lib/types/client/locales.d.ts +66 -0
- package/lib/types/client/persistence.d.ts +6 -1
- package/lib/types/client-discovery.d.ts +45 -0
- package/lib/types/clients.d.ts +17 -10
- package/lib/types/command.d.ts +41 -0
- package/lib/types/connections.d.ts +115 -40
- package/lib/types/defaults.d.ts +2 -0
- package/lib/types/index.d.ts +109 -63
- package/lib/types/routes.d.ts +25 -91
- package/lib/types/sql.d.ts +1 -1
- package/lib/types/storage.d.ts +70 -0
- package/lib/types/structured-read.d.ts +50 -0
- package/lib/types/tool.d.ts +29 -20
- package/lib/types/tui-connection-form.d.ts +98 -0
- package/package.json +65 -4
- package/preset/data-agent/agent.cordis.yml +22 -25
- package/preset/data-agent/preset.yml +1 -1
- package/lib/defaults-Bac6QvNt.js +0 -911
- package/lib/query-CmhTFklw.js +0 -86
package/lib/defaults-Bac6QvNt.js
DELETED
|
@@ -1,911 +0,0 @@
|
|
|
1
|
-
import z from "schemastery";
|
|
2
|
-
//#region src/sql.ts
|
|
3
|
-
/**
|
|
4
|
-
* Lightweight SQL-text scanning helpers shared by the sqlcmd tool half and
|
|
5
|
-
* the /query route. This is intentionally NOT a SQL parser: the scanner only
|
|
6
|
-
* understands lexical boundaries (strings, quoted identifiers, comments and
|
|
7
|
-
* parenthesis depth) well enough to make the two agent-loop guarantees from
|
|
8
|
-
* docs/optimization-opportunities.md:
|
|
9
|
-
*
|
|
10
|
-
* - a single tool call carries at most ONE SQL statement;
|
|
11
|
-
* - `maxRows` can be enforced with a real top-level LIMIT, not just a prompt.
|
|
12
|
-
*
|
|
13
|
-
* @module @yejiming/dsh-data-agent/sql
|
|
14
|
-
*/
|
|
15
|
-
const IDENT_CHAR = /[A-Za-z0-9_$]/;
|
|
16
|
-
function isWhitespace(char) {
|
|
17
|
-
return /\s/.test(char);
|
|
18
|
-
}
|
|
19
|
-
function isIdentChar(char) {
|
|
20
|
-
return IDENT_CHAR.test(char);
|
|
21
|
-
}
|
|
22
|
-
function skipQuoted(sql, start) {
|
|
23
|
-
const quote = sql[start];
|
|
24
|
-
let index = start + 1;
|
|
25
|
-
while (index < sql.length) {
|
|
26
|
-
const char = sql[index];
|
|
27
|
-
if (char === "\\" && index + 1 < sql.length && quote !== "`") {
|
|
28
|
-
index += 2;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
if (char === quote) {
|
|
32
|
-
if (sql[index + 1] === quote) {
|
|
33
|
-
index += 2;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
return index + 1;
|
|
37
|
-
}
|
|
38
|
-
index += 1;
|
|
39
|
-
}
|
|
40
|
-
return sql.length;
|
|
41
|
-
}
|
|
42
|
-
function skipDollarQuoted(sql, start) {
|
|
43
|
-
const match = sql.slice(start).match(/^\$[A-Za-z_][A-Za-z0-9_]*\$|^\$\$/);
|
|
44
|
-
if (match === null) return -1;
|
|
45
|
-
const delimiter = match[0];
|
|
46
|
-
const end = sql.indexOf(delimiter, start + delimiter.length);
|
|
47
|
-
return end === -1 ? sql.length : end + delimiter.length;
|
|
48
|
-
}
|
|
49
|
-
function skipOracleQuoted(sql, start) {
|
|
50
|
-
if (!/^q'/i.test(sql.slice(start, start + 2))) return -1;
|
|
51
|
-
const open = sql[start + 2];
|
|
52
|
-
if (open === void 0) return sql.length;
|
|
53
|
-
const close = {
|
|
54
|
-
"[": "]",
|
|
55
|
-
"{": "}",
|
|
56
|
-
"(": ")",
|
|
57
|
-
"<": ">"
|
|
58
|
-
}[open] ?? open;
|
|
59
|
-
let index = start + 3;
|
|
60
|
-
while (index < sql.length) {
|
|
61
|
-
if (sql[index] === close && sql[index + 1] === "'") return index + 2;
|
|
62
|
-
index += 1;
|
|
63
|
-
}
|
|
64
|
-
return sql.length;
|
|
65
|
-
}
|
|
66
|
-
function skipBlockComment(sql, start) {
|
|
67
|
-
let depth = 1;
|
|
68
|
-
let index = start + 2;
|
|
69
|
-
while (index < sql.length) {
|
|
70
|
-
if (sql.startsWith("/*", index)) {
|
|
71
|
-
depth += 1;
|
|
72
|
-
index += 2;
|
|
73
|
-
continue;
|
|
74
|
-
}
|
|
75
|
-
if (sql.startsWith("*/", index)) {
|
|
76
|
-
depth -= 1;
|
|
77
|
-
index += 2;
|
|
78
|
-
if (depth === 0) return index;
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
index += 1;
|
|
82
|
-
}
|
|
83
|
-
return sql.length;
|
|
84
|
-
}
|
|
85
|
-
function skipLineComment(sql, start) {
|
|
86
|
-
const newline = sql.indexOf("\n", start);
|
|
87
|
-
return newline === -1 ? sql.length : newline + 1;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* Walk the SQL text, invoking `onSemicolon` for every top-level statement
|
|
91
|
-
* separator (parenthesis depth zero, outside strings, quoted identifiers and
|
|
92
|
-
* comments).
|
|
93
|
-
*/
|
|
94
|
-
function scanTopLevelSemicolons(sql, onSemicolon) {
|
|
95
|
-
let depth = 0;
|
|
96
|
-
let index = 0;
|
|
97
|
-
while (index < sql.length) {
|
|
98
|
-
const char = sql[index];
|
|
99
|
-
if (isWhitespace(char)) {
|
|
100
|
-
index += 1;
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
if (sql.startsWith("--", index)) {
|
|
104
|
-
index = skipLineComment(sql, index + 2);
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (sql.startsWith("/*", index)) {
|
|
108
|
-
index = skipBlockComment(sql, index);
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
if (char === "'" || char === "\"" || char === "`") {
|
|
112
|
-
index = skipQuoted(sql, index);
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
if (char === "$") {
|
|
116
|
-
const dollarEnd = skipDollarQuoted(sql, index);
|
|
117
|
-
if (dollarEnd !== -1) {
|
|
118
|
-
index = dollarEnd;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const oracleEnd = skipOracleQuoted(sql, index);
|
|
123
|
-
if (oracleEnd !== -1) {
|
|
124
|
-
index = oracleEnd;
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
if (char === "(") {
|
|
128
|
-
depth += 1;
|
|
129
|
-
index += 1;
|
|
130
|
-
continue;
|
|
131
|
-
}
|
|
132
|
-
if (char === ")") {
|
|
133
|
-
depth = Math.max(0, depth - 1);
|
|
134
|
-
index += 1;
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (char === ";" && depth === 0) onSemicolon(index);
|
|
138
|
-
index += 1;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
/** Whether meaningful SQL content exists after `index` (trailing `;`/comments ignored). */
|
|
142
|
-
function hasContentAfter(sql, index) {
|
|
143
|
-
let cursor = index;
|
|
144
|
-
while (cursor < sql.length) {
|
|
145
|
-
const char = sql[cursor];
|
|
146
|
-
if (isWhitespace(char)) {
|
|
147
|
-
cursor += 1;
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
if (char === ";") {
|
|
151
|
-
cursor += 1;
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (sql.startsWith("--", cursor)) {
|
|
155
|
-
cursor = skipLineComment(sql, cursor + 2);
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
if (sql.startsWith("/*", cursor)) {
|
|
159
|
-
cursor = skipBlockComment(sql, cursor);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Throw unless `sql` contains at most one statement. A single trailing
|
|
168
|
-
* semicolon (and any number of repeated trailing semicolons / comments) is
|
|
169
|
-
* accepted; a semicolon followed by real content is rejected.
|
|
170
|
-
*/
|
|
171
|
-
function assertSingleStatement(sql, label = "SQL") {
|
|
172
|
-
if (stripTrailingTerminator(sql).trim().length === 0) throw new Error(`${label}: SQL 不能为空`);
|
|
173
|
-
const semicolons = [];
|
|
174
|
-
scanTopLevelSemicolons(sql, (index) => {
|
|
175
|
-
semicolons.push(index);
|
|
176
|
-
});
|
|
177
|
-
const offending = semicolons.find((index) => hasContentAfter(sql, index + 1));
|
|
178
|
-
if (offending === void 0) return;
|
|
179
|
-
throw new Error(`${label}: 一次只允许执行一条 SQL 语句(第 ${offending + 1} 个字符后的分号不是末尾分号)。多条语句请拆成多次调用;客户端进程独立、自动提交,不支持在多次调用间保持事务。`);
|
|
180
|
-
}
|
|
181
|
-
/** Whether `keyword` appears at top level as a whole word in `sql`. */
|
|
182
|
-
function hasTopLevelKeyword(sql, keyword) {
|
|
183
|
-
const needle = keyword.toLowerCase();
|
|
184
|
-
let depth = 0;
|
|
185
|
-
let index = 0;
|
|
186
|
-
while (index < sql.length) {
|
|
187
|
-
const char = sql[index];
|
|
188
|
-
if (isWhitespace(char)) {
|
|
189
|
-
index += 1;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
if (sql.startsWith("--", index)) {
|
|
193
|
-
index = skipLineComment(sql, index + 2);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
if (sql.startsWith("/*", index)) {
|
|
197
|
-
index = skipBlockComment(sql, index);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
if (char === "'" || char === "\"" || char === "`") {
|
|
201
|
-
index = skipQuoted(sql, index);
|
|
202
|
-
continue;
|
|
203
|
-
}
|
|
204
|
-
if (char === "$") {
|
|
205
|
-
const dollarEnd = skipDollarQuoted(sql, index);
|
|
206
|
-
if (dollarEnd !== -1) {
|
|
207
|
-
index = dollarEnd;
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
const oracleEnd = skipOracleQuoted(sql, index);
|
|
212
|
-
if (oracleEnd !== -1) {
|
|
213
|
-
index = oracleEnd;
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
if (char === "(") {
|
|
217
|
-
depth += 1;
|
|
218
|
-
index += 1;
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
if (char === ")") {
|
|
222
|
-
depth = Math.max(0, depth - 1);
|
|
223
|
-
index += 1;
|
|
224
|
-
continue;
|
|
225
|
-
}
|
|
226
|
-
if (depth === 0 && sql.slice(index, index + needle.length).toLowerCase() === needle && (index === 0 || !isIdentChar(sql[index - 1])) && (index + needle.length >= sql.length || !isIdentChar(sql[index + needle.length]))) return true;
|
|
227
|
-
index += 1;
|
|
228
|
-
}
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
231
|
-
function trailingLineCommentStart(sql, end) {
|
|
232
|
-
let index = sql.lastIndexOf("\n", end - 1) + 1;
|
|
233
|
-
while (index < end) {
|
|
234
|
-
const char = sql[index];
|
|
235
|
-
if (char === "'" || char === "\"" || char === "`") {
|
|
236
|
-
index = skipQuoted(sql, index);
|
|
237
|
-
continue;
|
|
238
|
-
}
|
|
239
|
-
if (sql.startsWith("--", index)) {
|
|
240
|
-
const tail = sql.slice(index + 2, end);
|
|
241
|
-
return tail.length === 0 || isWhitespace(tail[0]) ? index : -1;
|
|
242
|
-
}
|
|
243
|
-
index += 1;
|
|
244
|
-
}
|
|
245
|
-
return -1;
|
|
246
|
-
}
|
|
247
|
-
function blockCommentEndingAt(sql, end) {
|
|
248
|
-
let candidate = -1;
|
|
249
|
-
let index = 0;
|
|
250
|
-
while (index < end) {
|
|
251
|
-
const char = sql[index];
|
|
252
|
-
if (isWhitespace(char)) {
|
|
253
|
-
index += 1;
|
|
254
|
-
continue;
|
|
255
|
-
}
|
|
256
|
-
if (char === "'" || char === "\"" || char === "`") {
|
|
257
|
-
index = skipQuoted(sql, index);
|
|
258
|
-
continue;
|
|
259
|
-
}
|
|
260
|
-
if (sql.startsWith("--", index)) {
|
|
261
|
-
index = skipLineComment(sql, index + 2);
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
if (sql.startsWith("/*", index)) {
|
|
265
|
-
const start = index;
|
|
266
|
-
let commentDepth = 1;
|
|
267
|
-
index += 2;
|
|
268
|
-
while (index < end && commentDepth > 0) {
|
|
269
|
-
if (sql.startsWith("/*", index)) {
|
|
270
|
-
commentDepth += 1;
|
|
271
|
-
index += 2;
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
if (sql.startsWith("*/", index)) {
|
|
275
|
-
commentDepth -= 1;
|
|
276
|
-
index += 2;
|
|
277
|
-
if (commentDepth === 0) {
|
|
278
|
-
if (index === end) candidate = start;
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
continue;
|
|
282
|
-
}
|
|
283
|
-
index += 1;
|
|
284
|
-
}
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
index += 1;
|
|
288
|
-
}
|
|
289
|
-
return candidate;
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Strip trailing whitespace, statement terminators and trailing comments so a
|
|
293
|
-
* limit clause can be appended to the actual statement text. Only comments
|
|
294
|
-
* that occupy the whole tail are removed; the preceding statement is kept.
|
|
295
|
-
*/
|
|
296
|
-
function stripTrailingTerminator(sql) {
|
|
297
|
-
let end = sql.length;
|
|
298
|
-
for (;;) {
|
|
299
|
-
while (end > 0 && isWhitespace(sql[end - 1])) end -= 1;
|
|
300
|
-
if (end > 0 && sql[end - 1] === ";") {
|
|
301
|
-
end -= 1;
|
|
302
|
-
continue;
|
|
303
|
-
}
|
|
304
|
-
if (end >= 2 && sql.slice(end - 2, end) === "*/") {
|
|
305
|
-
const start = blockCommentEndingAt(sql, end);
|
|
306
|
-
if (start !== -1) {
|
|
307
|
-
end = start;
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
const lineComment = trailingLineCommentStart(sql, end);
|
|
312
|
-
if (lineComment !== -1) {
|
|
313
|
-
end = lineComment;
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
return sql.slice(0, end);
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
//#endregion
|
|
320
|
-
//#region src/clients.ts
|
|
321
|
-
/**
|
|
322
|
-
* Whitespace / comment stripping for {@link classifyStatement}: remove
|
|
323
|
-
* leading whitespace, `--` line comments, and nested `/* ... */` block
|
|
324
|
-
* comments so the first meaningful token can be read reliably.
|
|
325
|
-
*/
|
|
326
|
-
function stripLeadingComments(sql) {
|
|
327
|
-
let rest = sql;
|
|
328
|
-
for (;;) {
|
|
329
|
-
let changed = false;
|
|
330
|
-
const trimmed = rest.replace(/^\s+/, "");
|
|
331
|
-
if (trimmed !== rest) {
|
|
332
|
-
rest = trimmed;
|
|
333
|
-
changed = true;
|
|
334
|
-
}
|
|
335
|
-
if (rest.startsWith("--")) {
|
|
336
|
-
const newline = rest.indexOf("\n");
|
|
337
|
-
rest = newline === -1 ? "" : rest.slice(newline + 1);
|
|
338
|
-
changed = true;
|
|
339
|
-
continue;
|
|
340
|
-
}
|
|
341
|
-
if (rest.startsWith("/*")) {
|
|
342
|
-
const end = scanBlockCommentEnd(rest, 2);
|
|
343
|
-
rest = end === -1 ? "" : rest.slice(end);
|
|
344
|
-
changed = true;
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
if (!changed) {
|
|
348
|
-
const retrim = rest.replace(/^\s+/, "");
|
|
349
|
-
if (retrim !== rest) {
|
|
350
|
-
rest = retrim;
|
|
351
|
-
continue;
|
|
352
|
-
}
|
|
353
|
-
break;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return rest;
|
|
357
|
-
}
|
|
358
|
-
/** Find the index just past a `/* ... */` block starting at `start` (nesting-aware). */
|
|
359
|
-
function scanBlockCommentEnd(sql, start) {
|
|
360
|
-
let depth = 1;
|
|
361
|
-
let i = start;
|
|
362
|
-
while (i < sql.length) {
|
|
363
|
-
if (sql.startsWith("/*", i)) {
|
|
364
|
-
depth += 1;
|
|
365
|
-
i += 2;
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
if (sql.startsWith("*/", i)) {
|
|
369
|
-
depth -= 1;
|
|
370
|
-
i += 2;
|
|
371
|
-
if (depth === 0) return i;
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
i += 1;
|
|
375
|
-
}
|
|
376
|
-
return -1;
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* Strip a `WITH` prefix down to the main query: remove `WITH [RECURSIVE]`,
|
|
380
|
-
* then consume successive `name [ (cols) ] AS ( ... )` clauses (comma
|
|
381
|
-
* separated, parenthesis-aware) until the leading keyword of the main
|
|
382
|
-
* statement. Falls back to the whole (comment-stripped) input when the CTE
|
|
383
|
-
* shape does not parse cleanly, in which case {@link classifyStatement} treats
|
|
384
|
-
* it as a write (conservative).
|
|
385
|
-
*/
|
|
386
|
-
function stripWithBody(sql) {
|
|
387
|
-
let rest = stripLeadingComments(sql).replace(/^[A-Za-z_]+/, "");
|
|
388
|
-
rest = stripLeadingComments(rest);
|
|
389
|
-
if (/^RECURSIVE\b/i.test(rest)) rest = stripLeadingComments(rest.replace(/^[A-Za-z_]+/, ""));
|
|
390
|
-
for (;;) {
|
|
391
|
-
rest = stripLeadingComments(rest);
|
|
392
|
-
if (rest === "" || !/^[A-Za-z_][A-Za-z0-9_$]*/.test(rest)) break;
|
|
393
|
-
rest = stripLeadingComments(rest.replace(/^[A-Za-z_][A-Za-z0-9_$]*/, ""));
|
|
394
|
-
rest = stripLeadingComments(rest);
|
|
395
|
-
if (rest.startsWith("(")) {
|
|
396
|
-
const afterCols = skipParens(rest, 0);
|
|
397
|
-
rest = stripLeadingComments(afterCols === -1 ? rest : rest.slice(afterCols));
|
|
398
|
-
}
|
|
399
|
-
rest = stripLeadingComments(rest);
|
|
400
|
-
if (!/^AS\b/i.test(rest)) break;
|
|
401
|
-
rest = stripLeadingComments(rest.replace(/^[A-Za-z_]+/, ""));
|
|
402
|
-
rest = stripLeadingComments(rest);
|
|
403
|
-
if (!rest.startsWith("(")) break;
|
|
404
|
-
const afterBody = skipParens(rest, 0);
|
|
405
|
-
if (afterBody === -1) return sql;
|
|
406
|
-
rest = stripLeadingComments(rest.slice(afterBody));
|
|
407
|
-
rest = stripLeadingComments(rest);
|
|
408
|
-
if (rest.startsWith(",")) {
|
|
409
|
-
rest = stripLeadingComments(rest.slice(1));
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
break;
|
|
413
|
-
}
|
|
414
|
-
return stripLeadingComments(rest);
|
|
415
|
-
}
|
|
416
|
-
/** Index just past a balanced parenthesis group starting at `start` (0-based). */
|
|
417
|
-
function skipParens(sql, start) {
|
|
418
|
-
let depth = 0;
|
|
419
|
-
let i = start;
|
|
420
|
-
while (i < sql.length) {
|
|
421
|
-
const ch = sql[i];
|
|
422
|
-
if (ch === "(") {
|
|
423
|
-
depth += 1;
|
|
424
|
-
i += 1;
|
|
425
|
-
continue;
|
|
426
|
-
}
|
|
427
|
-
if (ch === ")") {
|
|
428
|
-
depth -= 1;
|
|
429
|
-
i += 1;
|
|
430
|
-
if (depth === 0) return i;
|
|
431
|
-
continue;
|
|
432
|
-
}
|
|
433
|
-
i += 1;
|
|
434
|
-
}
|
|
435
|
-
return -1;
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Classify a SQL text as a read or write statement by its FIRST effective
|
|
439
|
-
* token (a conservative read whitelist, not a parser). `with` is read only
|
|
440
|
-
* when its body's first token is `select`. SQLite `pragma` is read in its
|
|
441
|
-
* query form and write when a value is assigned.
|
|
442
|
-
*/
|
|
443
|
-
function classifyStatement(sql, type) {
|
|
444
|
-
const rest = stripLeadingComments(sql);
|
|
445
|
-
const tokenMatch = rest.match(/^[A-Za-z_]+/);
|
|
446
|
-
if (tokenMatch === null) return "write";
|
|
447
|
-
switch (tokenMatch[0].toLowerCase()) {
|
|
448
|
-
case "select":
|
|
449
|
-
case "show":
|
|
450
|
-
case "describe":
|
|
451
|
-
case "desc":
|
|
452
|
-
case "explain": return "read";
|
|
453
|
-
case "pragma": {
|
|
454
|
-
if (type !== "sqlite") return "write";
|
|
455
|
-
const afterPragma = rest.replace(/^pragma\b/i, "").trimStart();
|
|
456
|
-
return /^(?:[A-Za-z_][A-Za-z0-9_$]*(?:\s*\.\s*[A-Za-z_][A-Za-z0-9_$]*)?|"[^"]+"|`[^`]+`)\s*=/.test(afterPragma) ? "write" : "read";
|
|
457
|
-
}
|
|
458
|
-
case "with": return stripWithBody(rest).match(/^[A-Za-z_]+/)?.[0]?.toLowerCase() === "select" ? "read" : "write";
|
|
459
|
-
default: return "write";
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
/**
|
|
463
|
-
* Enforce the configured `maxRows` on a read query instead of relying on the
|
|
464
|
-
* prompt. SELECT/CTE-read statements get a real top-level LIMIT (Oracle uses
|
|
465
|
-
* a ROWNUM wrapper because it has no LIMIT); SHOW/DESCRIBE/EXPLAIN/PRAGMA are
|
|
466
|
-
* left untouched here and are capped while parsing structured output.
|
|
467
|
-
*
|
|
468
|
-
* An existing numeric top-level LIMIT is rewritten when it is larger than
|
|
469
|
-
* `maxRows`; a smaller existing LIMIT is preserved, and a non-numeric or
|
|
470
|
-
* unparseable LIMIT is left for the client (structured tools still truncate).
|
|
471
|
-
*/
|
|
472
|
-
function enforceReadRowLimit(sql, type, maxRows) {
|
|
473
|
-
if (classifyStatement(sql, type) !== "read") return sql;
|
|
474
|
-
const first = stripLeadingComments(sql).match(/^[A-Za-z_]+/)?.[0]?.toLowerCase();
|
|
475
|
-
if (first !== "select" && first !== "with") return sql;
|
|
476
|
-
const hadTrailingSemicolon = /;\s*$/.test(sql);
|
|
477
|
-
if (!hasTopLevelKeyword(sql, "limit") && type !== "oracle") return `${stripTrailingTerminator(sql)} LIMIT ${maxRows}${hadTrailingSemicolon ? ";" : ""}`;
|
|
478
|
-
if (type === "oracle") return `SELECT * FROM (${stripTrailingTerminator(sql)}) dsh_limit WHERE ROWNUM <= ${maxRows}${hadTrailingSemicolon ? ";" : ""}`;
|
|
479
|
-
if (!hasTopLevelKeyword(sql, "limit")) return sql;
|
|
480
|
-
return rewriteTopLevelLimit(sql, maxRows);
|
|
481
|
-
}
|
|
482
|
-
/** Rewrite the first top-level `LIMIT n` / `LIMIT n, m` with a capped row count. */
|
|
483
|
-
function rewriteTopLevelLimit(sql, maxRows) {
|
|
484
|
-
let depth = 0;
|
|
485
|
-
let index = 0;
|
|
486
|
-
while (index < sql.length) {
|
|
487
|
-
const char = sql[index];
|
|
488
|
-
if (/\s/.test(char)) {
|
|
489
|
-
index += 1;
|
|
490
|
-
continue;
|
|
491
|
-
}
|
|
492
|
-
if (sql.startsWith("--", index)) {
|
|
493
|
-
const newline = sql.indexOf("\n", index + 2);
|
|
494
|
-
index = newline === -1 ? sql.length : newline + 1;
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
if (sql.startsWith("/*", index)) {
|
|
498
|
-
let depthComment = 1;
|
|
499
|
-
index += 2;
|
|
500
|
-
while (index < sql.length && depthComment > 0) {
|
|
501
|
-
if (sql.startsWith("/*", index)) {
|
|
502
|
-
depthComment += 1;
|
|
503
|
-
index += 2;
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
if (sql.startsWith("*/", index)) {
|
|
507
|
-
depthComment -= 1;
|
|
508
|
-
index += 2;
|
|
509
|
-
continue;
|
|
510
|
-
}
|
|
511
|
-
index += 1;
|
|
512
|
-
}
|
|
513
|
-
continue;
|
|
514
|
-
}
|
|
515
|
-
if (char === "'" || char === "\"" || char === "`") {
|
|
516
|
-
const quote = char;
|
|
517
|
-
index += 1;
|
|
518
|
-
while (index < sql.length) {
|
|
519
|
-
if (sql[index] === "\\" && index + 1 < sql.length && quote !== "`") {
|
|
520
|
-
index += 2;
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
if (sql[index] === quote) {
|
|
524
|
-
if (sql[index + 1] === quote) {
|
|
525
|
-
index += 2;
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
index += 1;
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
index += 1;
|
|
532
|
-
}
|
|
533
|
-
continue;
|
|
534
|
-
}
|
|
535
|
-
if (char === "(") {
|
|
536
|
-
depth += 1;
|
|
537
|
-
index += 1;
|
|
538
|
-
continue;
|
|
539
|
-
}
|
|
540
|
-
if (char === ")") {
|
|
541
|
-
depth = Math.max(0, depth - 1);
|
|
542
|
-
index += 1;
|
|
543
|
-
continue;
|
|
544
|
-
}
|
|
545
|
-
if (depth === 0 && sql.slice(index, index + 5).toLowerCase() === "limit" && (index === 0 || !/[A-Za-z0-9_$]/.test(sql[index - 1])) && (index + 5 >= sql.length || !/[A-Za-z0-9_$]/.test(sql[index + 5]))) {
|
|
546
|
-
const match = sql.slice(index).match(/^LIMIT\s+(ALL|\d+)(\s*,\s*\d+)?/i);
|
|
547
|
-
if (match === null) return sql;
|
|
548
|
-
const firstValue = match[1];
|
|
549
|
-
const hasOffsetPart = match[2] !== void 0;
|
|
550
|
-
let replacement = "";
|
|
551
|
-
if (hasOffsetPart) {
|
|
552
|
-
const rowCount = Number(match[2].match(/\d+/)[0]);
|
|
553
|
-
replacement = `LIMIT ${firstValue === "ALL" ? "0" : firstValue}, ${Math.min(rowCount, maxRows)}`;
|
|
554
|
-
} else if (/^\d+$/.test(firstValue)) replacement = `LIMIT ${Math.min(Number(firstValue), maxRows)}`;
|
|
555
|
-
else replacement = `LIMIT ${maxRows}`;
|
|
556
|
-
return sql.slice(0, index) + replacement + sql.slice(index + match[0].length);
|
|
557
|
-
}
|
|
558
|
-
index += 1;
|
|
559
|
-
}
|
|
560
|
-
return sql;
|
|
561
|
-
}
|
|
562
|
-
/**
|
|
563
|
-
* Validate and quote one schema/table identifier for a safe metadata query.
|
|
564
|
-
* Identifiers are restricted to `[A-Za-z0-9_$]+` and then wrapped per type:
|
|
565
|
-
* backticks (mysql/hive/impala) or double quotes (postgres/oracle/sqlite),
|
|
566
|
-
* with the wrapping quote doubled for any interior occurrence. Rejects any
|
|
567
|
-
* input that could cross the identifier boundary (`#`, `--`, `;`, `'`, `` ` ``,
|
|
568
|
-
* `"`, `.`, `-` are all refused).
|
|
569
|
-
*/
|
|
570
|
-
function sanitizeIdentifier(type, identifier) {
|
|
571
|
-
if (!/^[A-Za-z0-9_$]+$/.test(identifier)) throw new Error(`标识符含非法字符(仅允许字母、数字与 _ $):${identifier}`);
|
|
572
|
-
switch (type) {
|
|
573
|
-
case "mysql":
|
|
574
|
-
case "hive":
|
|
575
|
-
case "impala": return "`" + identifier.replace(/`/g, "``") + "`";
|
|
576
|
-
case "postgres":
|
|
577
|
-
case "oracle":
|
|
578
|
-
case "sqlite": return "\"" + identifier.replace(/"/g, "\"\"") + "\"";
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
/**
|
|
582
|
-
* Quote one identifier-shaped value as a SQL string literal (single quotes,
|
|
583
|
-
* interior `'` doubled). postgres/oracle metadata queries filter system
|
|
584
|
-
* catalogs by NAME (a string value), not by identifier, so those positions
|
|
585
|
-
* need a quoted literal — not {@link sanitizeIdentifier}'s identifier quoting.
|
|
586
|
-
* The whitelist already excludes `'`, so doubling is a defense-in-depth no-op
|
|
587
|
-
* here but keeps the helper correct for any future widened charset.
|
|
588
|
-
*/
|
|
589
|
-
function quoteStringLiteral(value) {
|
|
590
|
-
return "'" + value.replace(/'/g, "''") + "'";
|
|
591
|
-
}
|
|
592
|
-
/** Loader schema for one client override (all fields optional at input). */
|
|
593
|
-
const clientConfigSchema = z.object({
|
|
594
|
-
command: z.string(),
|
|
595
|
-
args: z.array(z.string())
|
|
596
|
-
});
|
|
597
|
-
/** Loader schema for the whole `clients` config object (any type key). */
|
|
598
|
-
const clientsSchema = z.dict(clientConfigSchema).default({});
|
|
599
|
-
/** Query-mode flag arguments per type (plain/human output). */
|
|
600
|
-
const QUERY_ARGS = {
|
|
601
|
-
mysql: ["--batch", "--raw"],
|
|
602
|
-
postgres: ["-A"],
|
|
603
|
-
sqlite: ["-header", "-column"],
|
|
604
|
-
oracle: ["-S", "/nolog"],
|
|
605
|
-
hive: ["--silent=true", "--outputformat=tsv2"],
|
|
606
|
-
impala: ["-B"]
|
|
607
|
-
};
|
|
608
|
-
/** Introspection-mode flag arguments per type (machine-readable listing). */
|
|
609
|
-
const INTROSPECT_ARGS = {
|
|
610
|
-
mysql: ["--batch", "--raw"],
|
|
611
|
-
postgres: ["-t", "-A"],
|
|
612
|
-
sqlite: ["-noheader", "-list"],
|
|
613
|
-
oracle: ["-S", "/nolog"],
|
|
614
|
-
hive: ["--silent=true", "--outputformat=tsv2"],
|
|
615
|
-
impala: ["-B"]
|
|
616
|
-
};
|
|
617
|
-
/** Structured `sql-query` flag arguments: header + one row per line. */
|
|
618
|
-
const STRUCTURED_QUERY_ARGS = {
|
|
619
|
-
mysql: ["--batch", "--raw"],
|
|
620
|
-
postgres: ["-A"],
|
|
621
|
-
sqlite: ["-header", "-csv"],
|
|
622
|
-
oracle: ["-S", "/nolog"],
|
|
623
|
-
hive: ["--silent=true", "--outputformat=tsv2"],
|
|
624
|
-
impala: ["-B", "--print_header"]
|
|
625
|
-
};
|
|
626
|
-
/** Default ports when the connection does not carry one. */
|
|
627
|
-
const DEFAULT_PORTS = {
|
|
628
|
-
mysql: 3306,
|
|
629
|
-
postgres: 5432,
|
|
630
|
-
sqlite: 0,
|
|
631
|
-
oracle: 1521,
|
|
632
|
-
hive: 1e4,
|
|
633
|
-
impala: 21050
|
|
634
|
-
};
|
|
635
|
-
/** Built-in commands per type (also the loader defaults; see `src/defaults.ts`). */
|
|
636
|
-
const DEFAULT_CLIENTS_COMMAND = {
|
|
637
|
-
mysql: "mysql",
|
|
638
|
-
postgres: "psql",
|
|
639
|
-
sqlite: "sqlite3",
|
|
640
|
-
oracle: "sqlplus",
|
|
641
|
-
hive: "beeline",
|
|
642
|
-
impala: "impala-shell"
|
|
643
|
-
};
|
|
644
|
-
/**
|
|
645
|
-
* Connection flags for one type. Oracle and Hive carry NO connection flags:
|
|
646
|
-
* their endpoint + credentials travel in the stdin prefix; Impala takes
|
|
647
|
-
* `-i host:port -d db` on the argv. SQLite's `database` file is positional
|
|
648
|
-
* and must come AFTER the flags.
|
|
649
|
-
*/
|
|
650
|
-
function connectionArgs(type, connection) {
|
|
651
|
-
switch (type) {
|
|
652
|
-
case "mysql": return [
|
|
653
|
-
"-h",
|
|
654
|
-
connection.host ?? "127.0.0.1",
|
|
655
|
-
"-P",
|
|
656
|
-
String(connection.port ?? DEFAULT_PORTS.mysql),
|
|
657
|
-
"-u",
|
|
658
|
-
connection.user ?? "root",
|
|
659
|
-
"-D",
|
|
660
|
-
connection.database
|
|
661
|
-
];
|
|
662
|
-
case "postgres": return [
|
|
663
|
-
"-h",
|
|
664
|
-
connection.host ?? "127.0.0.1",
|
|
665
|
-
"-p",
|
|
666
|
-
String(connection.port ?? DEFAULT_PORTS.postgres),
|
|
667
|
-
"-U",
|
|
668
|
-
connection.user ?? "postgres",
|
|
669
|
-
"-d",
|
|
670
|
-
connection.database
|
|
671
|
-
];
|
|
672
|
-
case "sqlite": return [connection.database];
|
|
673
|
-
case "impala": return [
|
|
674
|
-
"-i",
|
|
675
|
-
`${connection.host ?? "127.0.0.1"}:${connection.port ?? DEFAULT_PORTS.impala}`,
|
|
676
|
-
"-d",
|
|
677
|
-
connection.database
|
|
678
|
-
];
|
|
679
|
-
case "oracle":
|
|
680
|
-
case "hive": return [];
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
/** Credential environment entries per type; absent password yields an empty env. */
|
|
684
|
-
function credentialEnv(type, connection) {
|
|
685
|
-
const password = connection.password;
|
|
686
|
-
if (password === void 0) return {};
|
|
687
|
-
switch (type) {
|
|
688
|
-
case "mysql": return { MYSQL_PWD: password };
|
|
689
|
-
case "postgres": return { PGPASSWORD: password };
|
|
690
|
-
case "sqlite":
|
|
691
|
-
case "oracle":
|
|
692
|
-
case "hive":
|
|
693
|
-
case "impala": return {};
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
/**
|
|
697
|
-
* The stdin prefix per type: Oracle and Hive establish the session here, so
|
|
698
|
-
* their credentials never appear in argv. Oracle also silences sqlplus
|
|
699
|
-
* decoration (PAGESIZE/FEEDBACK/HEADING) and pins the column separator to
|
|
700
|
-
* `|` for the describe parser; Hive connects through beeline's `!connect`.
|
|
701
|
-
*/
|
|
702
|
-
function stdinPrefix(type, connection) {
|
|
703
|
-
switch (type) {
|
|
704
|
-
case "oracle": return `${[
|
|
705
|
-
"SET PAGESIZE 0",
|
|
706
|
-
"SET FEEDBACK OFF",
|
|
707
|
-
"SET HEADING OFF",
|
|
708
|
-
"SET COLSEP '|'",
|
|
709
|
-
"SET TRIMSPOOL ON",
|
|
710
|
-
connection.user !== void 0 ? `connect ${connection.user}${connection.password !== void 0 ? `/${connection.password}` : ""}@${connection.host ?? "127.0.0.1"}:${connection.port ?? DEFAULT_PORTS.oracle}/${connection.database}` : ""
|
|
711
|
-
].filter((line) => line !== "").join("\n")}\n`;
|
|
712
|
-
case "hive": return connection.user !== void 0 ? `!connect jdbc:hive2://${connection.host ?? "127.0.0.1"}:${connection.port ?? DEFAULT_PORTS.hive}/${connection.database} ${connection.user} ${connection.password ?? ""}\n` : "";
|
|
713
|
-
case "mysql":
|
|
714
|
-
case "postgres":
|
|
715
|
-
case "sqlite":
|
|
716
|
-
case "impala": return "";
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
/**
|
|
720
|
-
* Oracle structured-query prefix: same connect block as {@link stdinPrefix},
|
|
721
|
-
* but with HEADING ON and UNDERLINE OFF so `sql-query` can read the column
|
|
722
|
-
* names from the first output line.
|
|
723
|
-
*/
|
|
724
|
-
function structuredStdinPrefix(type, connection) {
|
|
725
|
-
if (type !== "oracle") return stdinPrefix(type, connection);
|
|
726
|
-
return `${[
|
|
727
|
-
"SET PAGESIZE 0",
|
|
728
|
-
"SET FEEDBACK OFF",
|
|
729
|
-
"SET HEADING ON",
|
|
730
|
-
"SET UNDERLINE OFF",
|
|
731
|
-
"SET COLSEP '|'",
|
|
732
|
-
"SET TRIMSPOOL ON",
|
|
733
|
-
connection.user !== void 0 ? `connect ${connection.user}${connection.password !== void 0 ? `/${connection.password}` : ""}@${connection.host ?? "127.0.0.1"}:${connection.port ?? DEFAULT_PORTS.oracle}/${connection.database}` : ""
|
|
734
|
-
].filter((line) => line !== "").join("\n")}\n`;
|
|
735
|
-
}
|
|
736
|
-
/** Apply one deployment override's extra args in front of the built-in flags. */
|
|
737
|
-
function withOverrides(flags, override) {
|
|
738
|
-
if (override === void 0 || override.args === void 0) return flags;
|
|
739
|
-
return [...override.args, ...flags];
|
|
740
|
-
}
|
|
741
|
-
/**
|
|
742
|
-
* Build one client invocation for a query execution (plain output). Flags
|
|
743
|
-
* come BEFORE the connection arguments everywhere: sqlite3 takes
|
|
744
|
-
* `[options] <database>`, and putting flags first is harmless for the others.
|
|
745
|
-
*/
|
|
746
|
-
function buildClientTemplate(type, connection, override) {
|
|
747
|
-
return {
|
|
748
|
-
command: override?.command ?? DEFAULT_CLIENTS_COMMAND[type],
|
|
749
|
-
args: [...withOverrides(QUERY_ARGS[type], override), ...connectionArgs(type, connection)],
|
|
750
|
-
env: credentialEnv(type, connection),
|
|
751
|
-
stdinPrefix: stdinPrefix(type, connection)
|
|
752
|
-
};
|
|
753
|
-
}
|
|
754
|
-
/** Build one client invocation for metadata runs (machine-readable flags). */
|
|
755
|
-
function buildIntrospectTemplate(type, connection, override) {
|
|
756
|
-
return {
|
|
757
|
-
command: override?.command ?? DEFAULT_CLIENTS_COMMAND[type],
|
|
758
|
-
args: [...withOverrides(INTROSPECT_ARGS[type], override), ...connectionArgs(type, connection)],
|
|
759
|
-
env: credentialEnv(type, connection),
|
|
760
|
-
stdinPrefix: stdinPrefix(type, connection)
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
/**
|
|
764
|
-
* Build one client invocation for the structured `sql-query` tool: every
|
|
765
|
-
* supported client prints a header row followed by one row per line (mysql
|
|
766
|
-
* tab, postgres pipe, sqlite csv, oracle pipe, hive/impala tsv).
|
|
767
|
-
*/
|
|
768
|
-
function buildStructuredQueryTemplate(type, connection, override) {
|
|
769
|
-
return {
|
|
770
|
-
command: override?.command ?? DEFAULT_CLIENTS_COMMAND[type],
|
|
771
|
-
args: [...withOverrides(STRUCTURED_QUERY_ARGS[type], override), ...connectionArgs(type, connection)],
|
|
772
|
-
env: credentialEnv(type, connection),
|
|
773
|
-
stdinPrefix: structuredStdinPrefix(type, connection)
|
|
774
|
-
};
|
|
775
|
-
}
|
|
776
|
-
/**
|
|
777
|
-
* The table-listing SQL per type, run at /connect time to verify
|
|
778
|
-
* connectivity: the connected database's own tables (mysql uses the
|
|
779
|
-
* connection's database as the schema; postgres lists `public`; oracle lists
|
|
780
|
-
* the connected user's tables; hive/impala list the default database).
|
|
781
|
-
*/
|
|
782
|
-
function tableListingSql(type, connection) {
|
|
783
|
-
switch (type) {
|
|
784
|
-
case "mysql": return `SHOW TABLES FROM \`${connection?.database ?? ""}\`;`;
|
|
785
|
-
case "postgres": return "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY 1;";
|
|
786
|
-
case "sqlite": return "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;";
|
|
787
|
-
case "oracle": return "SELECT table_name FROM user_tables ORDER BY 1;";
|
|
788
|
-
case "hive":
|
|
789
|
-
case "impala": return "SHOW TABLES;";
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
/**
|
|
793
|
-
* Metadata query per kind × type. `schema`/`table` are identifier whitelist
|
|
794
|
-
* validated by the caller (`[A-Za-z0-9_$]`) before they reach here.
|
|
795
|
-
*/
|
|
796
|
-
function metadataQuery(kind, type, schema, table) {
|
|
797
|
-
switch (kind) {
|
|
798
|
-
case "schemas": switch (type) {
|
|
799
|
-
case "mysql": return "SHOW DATABASES;";
|
|
800
|
-
case "postgres": return "SELECT schema_name FROM information_schema.schemata ORDER BY 1;";
|
|
801
|
-
case "sqlite": return "SELECT 'main';";
|
|
802
|
-
case "oracle": return "SELECT username FROM all_users ORDER BY 1;";
|
|
803
|
-
case "hive":
|
|
804
|
-
case "impala": return "SHOW DATABASES;";
|
|
805
|
-
}
|
|
806
|
-
case "tables": switch (type) {
|
|
807
|
-
case "mysql": return `SHOW TABLES FROM ${sanitizeIdentifier(type, schema)};`;
|
|
808
|
-
case "postgres": return `SELECT tablename FROM pg_tables WHERE schemaname=${quoteStringLiteral(schema)} ORDER BY 1;`;
|
|
809
|
-
case "sqlite": return "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;";
|
|
810
|
-
case "oracle": return `SELECT table_name FROM all_tables WHERE owner=${quoteStringLiteral(schema)} ORDER BY 1;`;
|
|
811
|
-
case "hive":
|
|
812
|
-
case "impala": return `SHOW TABLES IN ${sanitizeIdentifier(type, schema)};`;
|
|
813
|
-
}
|
|
814
|
-
case "describe": switch (type) {
|
|
815
|
-
case "mysql": return `DESCRIBE ${sanitizeIdentifier(type, schema)}.${sanitizeIdentifier(type, table)};`;
|
|
816
|
-
case "postgres": return `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema=${quoteStringLiteral(schema)} AND table_name=${quoteStringLiteral(table)} ORDER BY ordinal_position;`;
|
|
817
|
-
case "sqlite": return `PRAGMA table_info(${sanitizeIdentifier(type, table)});`;
|
|
818
|
-
case "oracle": return `SELECT column_name, data_type, nullable FROM all_tab_columns WHERE owner=${quoteStringLiteral(schema)} AND table_name=${quoteStringLiteral(table)} ORDER BY column_id;`;
|
|
819
|
-
case "hive":
|
|
820
|
-
case "impala": return `DESCRIBE ${sanitizeIdentifier(type, schema)}.${sanitizeIdentifier(type, table)};`;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
/**
|
|
825
|
-
* Split one type's machine-readable listing output into trimmed lines.
|
|
826
|
-
* Header lines are stripped per type: mysql `--batch` prints a header row
|
|
827
|
-
* (skip 1); postgres `-t`, sqlite `-noheader`, oracle `SET HEADING OFF`,
|
|
828
|
-
* hive/impala batch modes print none (skip 0).
|
|
829
|
-
*/
|
|
830
|
-
function parseListing(type, stdout) {
|
|
831
|
-
const lines = stdout.split("\n");
|
|
832
|
-
const start = type === "mysql" ? 1 : 0;
|
|
833
|
-
const items = [];
|
|
834
|
-
for (let index = start; index < lines.length; index += 1) {
|
|
835
|
-
const name = lines[index].trim();
|
|
836
|
-
if (name.length > 0) items.push(name);
|
|
837
|
-
}
|
|
838
|
-
return items;
|
|
839
|
-
}
|
|
840
|
-
/** Parse one type's table-listing output (the /connect connectivity check). */
|
|
841
|
-
function parseTableListing(type, stdout) {
|
|
842
|
-
return parseListing(type, stdout);
|
|
843
|
-
}
|
|
844
|
-
/**
|
|
845
|
-
* Parse one type's describe output into columns. Formats:
|
|
846
|
-
* - mysql `--batch`: `Field\tType\tNull\tKey\t...` (skip header);
|
|
847
|
-
* - postgres `-t -A`: `name|type|is_nullable`;
|
|
848
|
-
* - sqlite `-noheader -list`: `cid|name|type|notnull|dflt|pk` (name is part 1);
|
|
849
|
-
* - oracle (`SET COLSEP '|'`, heading off): `NAME|TYPE|NULLABLE`;
|
|
850
|
-
* - hive/impala batch: `name\ttype\tcomment`.
|
|
851
|
-
*/
|
|
852
|
-
function parseColumns(type, stdout) {
|
|
853
|
-
const lines = stdout.split("\n");
|
|
854
|
-
const start = type === "mysql" ? 1 : 0;
|
|
855
|
-
const columns = [];
|
|
856
|
-
for (let index = start; index < lines.length; index += 1) {
|
|
857
|
-
const line = lines[index].trim();
|
|
858
|
-
if (line.length === 0) continue;
|
|
859
|
-
const parts = line.includes(" ") ? line.split(" ") : line.split("|");
|
|
860
|
-
const nameIndex = type === "sqlite" ? 1 : 0;
|
|
861
|
-
const name = parts[nameIndex]?.trim() ?? "";
|
|
862
|
-
const columnType = parts[nameIndex + 1]?.trim() ?? "";
|
|
863
|
-
if (name.length === 0) continue;
|
|
864
|
-
const rawNullable = parts[nameIndex + 2]?.trim().toLowerCase();
|
|
865
|
-
let nullable;
|
|
866
|
-
switch (type) {
|
|
867
|
-
case "mysql":
|
|
868
|
-
nullable = rawNullable === "yes";
|
|
869
|
-
break;
|
|
870
|
-
case "postgres":
|
|
871
|
-
nullable = rawNullable === "yes";
|
|
872
|
-
break;
|
|
873
|
-
case "sqlite":
|
|
874
|
-
nullable = rawNullable !== "1";
|
|
875
|
-
break;
|
|
876
|
-
case "oracle":
|
|
877
|
-
nullable = rawNullable === "y";
|
|
878
|
-
break;
|
|
879
|
-
case "hive":
|
|
880
|
-
case "impala": nullable = void 0;
|
|
881
|
-
}
|
|
882
|
-
columns.push({
|
|
883
|
-
name,
|
|
884
|
-
type: columnType,
|
|
885
|
-
...nullable !== void 0 ? { nullable } : {}
|
|
886
|
-
});
|
|
887
|
-
}
|
|
888
|
-
return columns;
|
|
889
|
-
}
|
|
890
|
-
//#endregion
|
|
891
|
-
//#region src/defaults.ts
|
|
892
|
-
/**
|
|
893
|
-
* Package-wide defaults shared by the server half (`src/index.ts`) and the
|
|
894
|
-
* database tool half (`src/tool.ts`). Loader schemas carry these as their
|
|
895
|
-
* defaults so a deployment may override every one of them in cordis.yml.
|
|
896
|
-
* @module @yejiming/dsh-data-agent/defaults
|
|
897
|
-
*/
|
|
898
|
-
/** Preset directory name installed into `$DSH_HOME/.agent-presets/`. */
|
|
899
|
-
const DEFAULT_PRESET_ID = "data-agent";
|
|
900
|
-
/** End-to-end deadline for one `/connect` connectivity check, milliseconds. */
|
|
901
|
-
const DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
902
|
-
/** End-to-end deadline for one database-tool query, milliseconds. */
|
|
903
|
-
const DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
904
|
-
/** In-memory cap on database-tool captured output (stdout and stderr each). */
|
|
905
|
-
const DEFAULT_MAX_RESULT_CHARS = 2e4;
|
|
906
|
-
/** Cap on one /query SQL text length (abuse guard; the wire body stays small). */
|
|
907
|
-
const DEFAULT_MAX_QUERY_CHARS = 65536;
|
|
908
|
-
/** Grace period for the subprocess terminate escalation. */
|
|
909
|
-
const DEFAULT_GRACE_MS = 5e3;
|
|
910
|
-
//#endregion
|
|
911
|
-
export { sanitizeIdentifier as _, DEFAULT_PRESET_ID as a, buildIntrospectTemplate as c, clientsSchema as d, enforceReadRowLimit as f, parseTableListing as g, parseListing as h, DEFAULT_MAX_RESULT_CHARS as i, buildStructuredQueryTemplate as l, parseColumns as m, DEFAULT_GRACE_MS as n, DEFAULT_QUERY_TIMEOUT_MS as o, metadataQuery as p, DEFAULT_MAX_QUERY_CHARS as r, buildClientTemplate as s, DEFAULT_CONNECT_TIMEOUT_MS as t, classifyStatement as u, tableListingSql as v, assertSingleStatement as y };
|