@yejiming/dsh-data-agent 0.0.11 → 0.0.13
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 +70 -15
- package/README.md +70 -15
- package/conformance/dsh-ecosystem/baseline.json +59 -0
- package/conformance/dsh-ecosystem/dependencies.json +41 -0
- package/conformance/dsh-ecosystem/fixtures/host-degraded.fixture.json +12 -0
- package/conformance/dsh-ecosystem/fixtures/host-eligible.fixture.json +13 -0
- package/conformance/dsh-ecosystem/fixtures/host-rejected.fixture.json +12 -0
- package/conformance/dsh-ecosystem/fixtures/profiles/native-only/package.json +8 -0
- package/conformance/dsh-ecosystem/fixtures/profiles/native-plus-adapter/package.json +9 -0
- package/conformance/dsh-ecosystem/inventory.json +77 -0
- package/conformance/dsh-ecosystem/restrictions.json +20 -0
- package/dsh-plugin.json +67 -0
- package/lib/client.js +1352 -130
- package/lib/client.js.map +1 -1
- package/lib/{command-DuCpwVbl.js → command-utC5MHd9.js} +101 -60
- package/lib/command.js +1 -1
- package/lib/{connections-5sfdEDsG.js → connections-CHY4uB6z.js} +747 -65
- package/lib/defaults-Cngd8Tf8.js +131 -0
- package/lib/ecosystem.js +19 -0
- package/lib/index.js +40 -33
- package/lib/routes.js +5 -6
- package/lib/{tool-DVh61An-.js → tool-ZTOS4B33.js} +161 -177
- package/lib/tool.js +1 -1
- package/lib/types/analysis-html.d.ts +27 -0
- package/lib/types/analysis.d.ts +13 -1
- package/lib/types/client/DataAgentWorkbench.d.ts +1 -2
- package/lib/types/client/QueryResultTable.d.ts +13 -0
- package/lib/types/client/locales.d.ts +48 -0
- package/lib/types/client/persistence.d.ts +3 -1
- package/lib/types/client/query-export.d.ts +11 -0
- package/lib/types/client-discovery.d.ts +3 -4
- package/lib/types/clients.d.ts +14 -8
- package/lib/types/command.d.ts +2 -2
- package/lib/types/connections.d.ts +33 -4
- package/lib/types/database-types.d.ts +23 -0
- package/lib/types/defaults.d.ts +4 -0
- package/lib/types/ecosystem.d.ts +13 -0
- package/lib/types/index.d.ts +21 -16
- package/lib/types/presentation-text.d.ts +3 -0
- package/lib/types/query.d.ts +13 -11
- package/lib/types/sql.d.ts +9 -1
- package/lib/types/storage.d.ts +11 -1
- package/lib/types/structured-read.d.ts +2 -2
- package/lib/types/structured.d.ts +1 -1
- package/lib/types/tool.d.ts +5 -5
- package/lib/types/tui-connection-form.d.ts +10 -7
- package/package.json +66 -42
- package/preset/data-agent/agent.cordis.yml +9 -4
- package/lib/defaults-DP4RyRh1.js +0 -21
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import "./defaults-
|
|
1
|
+
import { c as WORKBENCH_MAX_RESULT_CHARS, d as defaultDatabasePort, f as defaultDatabaseUser, p as isDatabaseType, s as WORKBENCH_MAX_EXPORT_ROWS } from "./defaults-Cngd8Tf8.js";
|
|
2
2
|
import { readdir } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { posix, resolve, win32 } from "node:path";
|
|
5
5
|
import z from "schemastery";
|
|
6
6
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
7
|
+
import { createClient } from "@clickhouse/client";
|
|
7
8
|
//#region src/sql.ts
|
|
8
9
|
/**
|
|
9
10
|
* Lightweight SQL-text scanning helpers shared by the sql-cmd tool half and
|
|
@@ -13,7 +14,7 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
|
13
14
|
* docs/optimization-opportunities.md:
|
|
14
15
|
*
|
|
15
16
|
* - a single tool call carries at most ONE SQL statement;
|
|
16
|
-
* - `maxRows` can be enforced with a real
|
|
17
|
+
* - `maxRows` can be enforced with a real dialect-level row bound, not just a prompt.
|
|
17
18
|
*
|
|
18
19
|
* @module @yejiming/dsh-data-agent/sql
|
|
19
20
|
*/
|
|
@@ -233,6 +234,82 @@ function hasTopLevelKeyword(sql, keyword) {
|
|
|
233
234
|
}
|
|
234
235
|
return false;
|
|
235
236
|
}
|
|
237
|
+
/**
|
|
238
|
+
* Preserve executable SQL text while replacing strings, quoted identifiers,
|
|
239
|
+
* dollar/Oracle quoted bodies, and comments with spaces. Newlines are kept so
|
|
240
|
+
* line-oriented client directives can be checked without false positives.
|
|
241
|
+
*/
|
|
242
|
+
function maskSqlLiteralsAndComments(sql) {
|
|
243
|
+
const chars = sql.split("");
|
|
244
|
+
const mask = (start, end) => {
|
|
245
|
+
for (let index = start; index < end; index += 1) if (chars[index] !== "\n" && chars[index] !== "\r") chars[index] = " ";
|
|
246
|
+
};
|
|
247
|
+
let index = 0;
|
|
248
|
+
while (index < sql.length) {
|
|
249
|
+
const char = sql[index];
|
|
250
|
+
if (sql.startsWith("--", index)) {
|
|
251
|
+
const end = skipLineComment(sql, index + 2);
|
|
252
|
+
mask(index, end);
|
|
253
|
+
index = end;
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (sql.startsWith("/*", index)) {
|
|
257
|
+
const end = skipBlockComment(sql, index);
|
|
258
|
+
mask(index, end);
|
|
259
|
+
index = end;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (char === "'" || char === "\"" || char === "`") {
|
|
263
|
+
const end = skipQuoted(sql, index);
|
|
264
|
+
mask(index, end);
|
|
265
|
+
index = end;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (char === "[") {
|
|
269
|
+
let end = index + 1;
|
|
270
|
+
while (end < sql.length) {
|
|
271
|
+
if (sql[end] === "]" && sql[end + 1] === "]") {
|
|
272
|
+
end += 2;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (sql[end] === "]") {
|
|
276
|
+
end += 1;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
end += 1;
|
|
280
|
+
}
|
|
281
|
+
mask(index, end);
|
|
282
|
+
index = end;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (char === "$") {
|
|
286
|
+
const end = skipDollarQuoted(sql, index);
|
|
287
|
+
if (end !== -1) {
|
|
288
|
+
mask(index, end);
|
|
289
|
+
index = end;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const oracleEnd = skipOracleQuoted(sql, index);
|
|
294
|
+
if (oracleEnd !== -1) {
|
|
295
|
+
mask(index, oracleEnd);
|
|
296
|
+
index = oracleEnd;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
index += 1;
|
|
300
|
+
}
|
|
301
|
+
return chars.join("");
|
|
302
|
+
}
|
|
303
|
+
/** Reject commands interpreted by sqlcmd itself rather than by SQL Server. */
|
|
304
|
+
function assertSqlServerSafeInput(sql, label = "SQL Server SQL") {
|
|
305
|
+
const executable = maskSqlLiteralsAndComments(sql);
|
|
306
|
+
if (/\$\([^\r\n)]*\)/.test(executable)) throw new Error(`${label}: 禁止 sqlcmd 变量替换 $(...)`);
|
|
307
|
+
for (const line of executable.split(/\r?\n/)) {
|
|
308
|
+
const command = line.trimStart();
|
|
309
|
+
if (command === "") continue;
|
|
310
|
+
if (/^!!/.test(command) || /^:/.test(command) || /^(?:reset|ed|exit|quit)\b/i.test(command) || /^go(?:\s+\d+)?\s*;?\s*$/i.test(command)) throw new Error(`${label}: 禁止 sqlcmd 元命令、GO 批次分隔符与客户端脚本指令`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
236
313
|
function trailingLineCommentStart(sql, end) {
|
|
237
314
|
let index = sql.lastIndexOf("\n", end - 1) + 1;
|
|
238
315
|
while (index < end) {
|
|
@@ -449,7 +526,11 @@ function classifyStatement(sql, type) {
|
|
|
449
526
|
const rest = stripLeadingComments(sql);
|
|
450
527
|
const tokenMatch = rest.match(/^[A-Za-z_]+/);
|
|
451
528
|
if (tokenMatch === null) return "write";
|
|
452
|
-
|
|
529
|
+
const token = tokenMatch[0].toLowerCase();
|
|
530
|
+
const executable = maskSqlLiteralsAndComments(rest);
|
|
531
|
+
if (type === "sqlserver" && /\binto\b/i.test(executable)) return "write";
|
|
532
|
+
if ((type === "mysql" || type === "doris" || type === "clickhouse") && /\binto\s+(?:out|dump)file\b/i.test(executable)) return "write";
|
|
533
|
+
switch (token) {
|
|
453
534
|
case "select":
|
|
454
535
|
case "show":
|
|
455
536
|
case "describe":
|
|
@@ -478,12 +559,69 @@ function enforceReadRowLimit(sql, type, maxRows) {
|
|
|
478
559
|
if (classifyStatement(sql, type) !== "read") return sql;
|
|
479
560
|
const first = stripLeadingComments(sql).match(/^[A-Za-z_]+/)?.[0]?.toLowerCase();
|
|
480
561
|
if (first !== "select" && first !== "with") return sql;
|
|
562
|
+
if (type === "sqlserver") return enforceSqlServerRowLimit(sql, maxRows);
|
|
481
563
|
const hadTrailingSemicolon = /;\s*$/.test(sql);
|
|
482
564
|
if (!hasTopLevelKeyword(sql, "limit") && type !== "oracle") return `${stripTrailingTerminator(sql)} LIMIT ${maxRows}${hadTrailingSemicolon ? ";" : ""}`;
|
|
483
565
|
if (type === "oracle") return `SELECT * FROM (${stripTrailingTerminator(sql)}) dsh_limit WHERE ROWNUM <= ${maxRows}${hadTrailingSemicolon ? ";" : ""}`;
|
|
484
566
|
if (!hasTopLevelKeyword(sql, "limit")) return sql;
|
|
485
567
|
return rewriteTopLevelLimit(sql, maxRows);
|
|
486
568
|
}
|
|
569
|
+
function findTopLevelKeywordIndex(sql, keyword) {
|
|
570
|
+
const masked = maskSqlLiteralsAndComments(sql);
|
|
571
|
+
const needle = keyword.toLowerCase();
|
|
572
|
+
let depth = 0;
|
|
573
|
+
for (let index = 0; index < masked.length; index += 1) {
|
|
574
|
+
const char = masked[index];
|
|
575
|
+
if (char === "(") {
|
|
576
|
+
depth += 1;
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
if (char === ")") {
|
|
580
|
+
depth = Math.max(0, depth - 1);
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
if (depth !== 0) continue;
|
|
584
|
+
if (masked.slice(index, index + needle.length).toLowerCase() !== needle) continue;
|
|
585
|
+
const before = index === 0 ? "" : masked[index - 1];
|
|
586
|
+
const after = masked[index + needle.length] ?? "";
|
|
587
|
+
if ((before === "" || !/[A-Za-z0-9_$]/.test(before)) && (after === "" || !/[A-Za-z0-9_$]/.test(after))) return index;
|
|
588
|
+
}
|
|
589
|
+
return -1;
|
|
590
|
+
}
|
|
591
|
+
/** Add or tighten a T-SQL row limit without ever emitting MySQL LIMIT. */
|
|
592
|
+
function enforceSqlServerRowLimit(sql, maxRows) {
|
|
593
|
+
const hadTrailingSemicolon = /;\s*$/.test(sql);
|
|
594
|
+
const body = stripTrailingTerminator(sql);
|
|
595
|
+
for (const keyword of [
|
|
596
|
+
"union",
|
|
597
|
+
"intersect",
|
|
598
|
+
"except"
|
|
599
|
+
]) if (hasTopLevelKeyword(body, keyword)) throw new Error("SQL Server compound query 无法安全自动限行,请显式包装查询并使用 TOP");
|
|
600
|
+
const selectIndex = findTopLevelKeywordIndex(body, "select");
|
|
601
|
+
if (selectIndex === -1) throw new Error("SQL Server 查询无法定位顶层 SELECT,无法安全自动限行");
|
|
602
|
+
const offsetIndex = findTopLevelKeywordIndex(body, "offset");
|
|
603
|
+
const fetchIndex = findTopLevelKeywordIndex(body, "fetch");
|
|
604
|
+
if (offsetIndex !== -1 || fetchIndex !== -1) {
|
|
605
|
+
if (offsetIndex === -1 || fetchIndex === -1 || fetchIndex < offsetIndex) throw new Error("SQL Server OFFSET/FETCH 查询无法安全自动改写,请使用完整的 OFFSET ... FETCH NEXT n ROWS ONLY");
|
|
606
|
+
const fetch = body.slice(fetchIndex).match(/^fetch\s+next\s+(\d+)\s+rows?\s+only\b/i);
|
|
607
|
+
if (fetch === null) throw new Error("SQL Server OFFSET/FETCH 查询无法安全自动改写,请显式设置数字 FETCH NEXT");
|
|
608
|
+
if (Number(fetch[1]) <= maxRows) return sql;
|
|
609
|
+
const replacement = fetch[0].replace(fetch[1], String(maxRows));
|
|
610
|
+
return `${body.slice(0, fetchIndex)}${replacement}${body.slice(fetchIndex + fetch[0].length)}${hadTrailingSemicolon ? ";" : ""}`;
|
|
611
|
+
}
|
|
612
|
+
const prefixMatch = body.slice(selectIndex + 6).match(/^(\s+(?:all\s+|distinct\s+)?)(?:top\s*(?:\(\s*(\d+)\s*\)|(\d+))(\s+percent)?(\s+with\s+ties)?\s*)?/i);
|
|
613
|
+
if (prefixMatch === null) throw new Error("SQL Server SELECT 形态无法安全自动限行");
|
|
614
|
+
const existing = prefixMatch[2] ?? prefixMatch[3];
|
|
615
|
+
if (prefixMatch[4] !== void 0 || prefixMatch[5] !== void 0) throw new Error("SQL Server TOP PERCENT/WITH TIES 无法安全自动限行,请改用显式 TOP (n)");
|
|
616
|
+
if (existing !== void 0) {
|
|
617
|
+
if (Number(existing) <= maxRows) return sql;
|
|
618
|
+
const topStart = selectIndex + 6 + prefixMatch[1].length;
|
|
619
|
+
const topLength = prefixMatch[0].length - prefixMatch[1].length;
|
|
620
|
+
return `${body.slice(0, topStart)}TOP (${maxRows}) ${body.slice(topStart + topLength)}${hadTrailingSemicolon ? ";" : ""}`;
|
|
621
|
+
}
|
|
622
|
+
const insertAt = selectIndex + 6 + prefixMatch[1].length;
|
|
623
|
+
return `${body.slice(0, insertAt)}TOP (${maxRows}) ${body.slice(insertAt)}${hadTrailingSemicolon ? ";" : ""}`;
|
|
624
|
+
}
|
|
487
625
|
/** Rewrite the first top-level `LIMIT n` / `LIMIT n, m` with a capped row count. */
|
|
488
626
|
function rewriteTopLevelLimit(sql, maxRows) {
|
|
489
627
|
let depth = 0;
|
|
@@ -576,11 +714,14 @@ function sanitizeIdentifier(type, identifier) {
|
|
|
576
714
|
if (!/^[A-Za-z0-9_$]+$/.test(identifier)) throw new Error(`标识符含非法字符(仅允许字母、数字与 _ $):${identifier}`);
|
|
577
715
|
switch (type) {
|
|
578
716
|
case "mysql":
|
|
717
|
+
case "doris":
|
|
718
|
+
case "clickhouse":
|
|
579
719
|
case "hive":
|
|
580
720
|
case "impala": return "`" + identifier.replace(/`/g, "``") + "`";
|
|
581
721
|
case "postgres":
|
|
582
722
|
case "oracle":
|
|
583
723
|
case "sqlite": return "\"" + identifier.replace(/"/g, "\"\"") + "\"";
|
|
724
|
+
case "sqlserver": return "[" + identifier.replace(/]/g, "]]") + "]";
|
|
584
725
|
}
|
|
585
726
|
}
|
|
586
727
|
/**
|
|
@@ -600,43 +741,100 @@ const clientConfigSchema = z.object({
|
|
|
600
741
|
args: z.array(z.string()),
|
|
601
742
|
searchPaths: z.array(z.string())
|
|
602
743
|
});
|
|
603
|
-
/** Loader schema for
|
|
604
|
-
const
|
|
744
|
+
/** Loader schema for CLI overrides; ClickHouse has connection-level HTTP transport instead. */
|
|
745
|
+
const cliDatabaseTypeSchema = z.union([
|
|
746
|
+
z.const("mysql"),
|
|
747
|
+
z.const("postgres"),
|
|
748
|
+
z.const("sqlite"),
|
|
749
|
+
z.const("oracle"),
|
|
750
|
+
z.const("hive"),
|
|
751
|
+
z.const("impala"),
|
|
752
|
+
z.const("doris"),
|
|
753
|
+
z.const("sqlserver")
|
|
754
|
+
]);
|
|
755
|
+
const clientsSchema = z.dict(clientConfigSchema, cliDatabaseTypeSchema).default({});
|
|
756
|
+
/**
|
|
757
|
+
* MySQL output must match the subprocess collector's UTF-8 decoder instead of
|
|
758
|
+
* inheriting a platform locale such as a legacy Windows code page.
|
|
759
|
+
*/
|
|
760
|
+
const MYSQL_COMMON_ARGS = [
|
|
761
|
+
"--default-character-set=utf8mb4",
|
|
762
|
+
"--batch",
|
|
763
|
+
"--raw"
|
|
764
|
+
];
|
|
605
765
|
/** Query-mode flag arguments per type (plain/human output). */
|
|
606
766
|
const QUERY_ARGS = {
|
|
607
|
-
mysql:
|
|
767
|
+
mysql: MYSQL_COMMON_ARGS,
|
|
768
|
+
doris: MYSQL_COMMON_ARGS,
|
|
608
769
|
postgres: ["-A"],
|
|
609
770
|
sqlite: ["-header", "-column"],
|
|
610
771
|
oracle: ["-S", "/nolog"],
|
|
611
772
|
hive: ["--silent=true", "--outputformat=tsv2"],
|
|
612
|
-
impala: ["-B"]
|
|
773
|
+
impala: ["-B"],
|
|
774
|
+
sqlserver: [
|
|
775
|
+
"-b",
|
|
776
|
+
"-V",
|
|
777
|
+
"11",
|
|
778
|
+
"-r",
|
|
779
|
+
"1",
|
|
780
|
+
"-x",
|
|
781
|
+
"-W",
|
|
782
|
+
"-w",
|
|
783
|
+
"65535",
|
|
784
|
+
"-s",
|
|
785
|
+
""
|
|
786
|
+
],
|
|
787
|
+
clickhouse: []
|
|
613
788
|
};
|
|
614
789
|
/** Introspection-mode flag arguments per type (machine-readable listing). */
|
|
615
790
|
const INTROSPECT_ARGS = {
|
|
616
|
-
mysql:
|
|
791
|
+
mysql: MYSQL_COMMON_ARGS,
|
|
792
|
+
doris: MYSQL_COMMON_ARGS,
|
|
617
793
|
postgres: ["-t", "-A"],
|
|
618
794
|
sqlite: ["-noheader", "-list"],
|
|
619
795
|
oracle: ["-S", "/nolog"],
|
|
620
796
|
hive: ["--silent=true", "--outputformat=tsv2"],
|
|
621
|
-
impala: ["-B"]
|
|
797
|
+
impala: ["-B"],
|
|
798
|
+
sqlserver: [
|
|
799
|
+
"-b",
|
|
800
|
+
"-V",
|
|
801
|
+
"11",
|
|
802
|
+
"-r",
|
|
803
|
+
"1",
|
|
804
|
+
"-x",
|
|
805
|
+
"-W",
|
|
806
|
+
"-w",
|
|
807
|
+
"65535",
|
|
808
|
+
"-s",
|
|
809
|
+
"",
|
|
810
|
+
"-h",
|
|
811
|
+
"-1"
|
|
812
|
+
],
|
|
813
|
+
clickhouse: []
|
|
622
814
|
};
|
|
623
815
|
/** Structured `sql-query` flag arguments: header + one row per line. */
|
|
624
816
|
const STRUCTURED_QUERY_ARGS = {
|
|
625
|
-
mysql:
|
|
817
|
+
mysql: MYSQL_COMMON_ARGS,
|
|
818
|
+
doris: MYSQL_COMMON_ARGS,
|
|
626
819
|
postgres: ["-A"],
|
|
627
820
|
sqlite: ["-header", "-csv"],
|
|
628
821
|
oracle: ["-S", "/nolog"],
|
|
629
822
|
hive: ["--silent=true", "--outputformat=tsv2"],
|
|
630
|
-
impala: ["-B", "--print_header"]
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
823
|
+
impala: ["-B", "--print_header"],
|
|
824
|
+
sqlserver: [
|
|
825
|
+
"-b",
|
|
826
|
+
"-V",
|
|
827
|
+
"11",
|
|
828
|
+
"-r",
|
|
829
|
+
"1",
|
|
830
|
+
"-x",
|
|
831
|
+
"-W",
|
|
832
|
+
"-w",
|
|
833
|
+
"65535",
|
|
834
|
+
"-s",
|
|
835
|
+
""
|
|
836
|
+
],
|
|
837
|
+
clickhouse: []
|
|
640
838
|
};
|
|
641
839
|
/** Built-in commands per type (also the loader defaults; see `src/defaults.ts`). */
|
|
642
840
|
const DEFAULT_CLIENTS_COMMAND = {
|
|
@@ -645,7 +843,10 @@ const DEFAULT_CLIENTS_COMMAND = {
|
|
|
645
843
|
sqlite: "sqlite3",
|
|
646
844
|
oracle: "sqlplus",
|
|
647
845
|
hive: "beeline",
|
|
648
|
-
impala: "impala-shell"
|
|
846
|
+
impala: "impala-shell",
|
|
847
|
+
doris: "mysql",
|
|
848
|
+
sqlserver: "sqlcmd",
|
|
849
|
+
clickhouse: ""
|
|
649
850
|
};
|
|
650
851
|
/**
|
|
651
852
|
* Connection flags for one type. Oracle and Hive carry NO connection flags:
|
|
@@ -655,13 +856,14 @@ const DEFAULT_CLIENTS_COMMAND = {
|
|
|
655
856
|
*/
|
|
656
857
|
function connectionArgs(type, connection) {
|
|
657
858
|
switch (type) {
|
|
658
|
-
case "mysql":
|
|
859
|
+
case "mysql":
|
|
860
|
+
case "doris": return [
|
|
659
861
|
"-h",
|
|
660
862
|
connection.host ?? "127.0.0.1",
|
|
661
863
|
"-P",
|
|
662
|
-
String(connection.port ??
|
|
864
|
+
String(connection.port ?? defaultDatabasePort(type)),
|
|
663
865
|
"-u",
|
|
664
|
-
connection.user ??
|
|
866
|
+
connection.user ?? defaultDatabaseUser(type),
|
|
665
867
|
"-D",
|
|
666
868
|
connection.database
|
|
667
869
|
];
|
|
@@ -669,7 +871,7 @@ function connectionArgs(type, connection) {
|
|
|
669
871
|
"-h",
|
|
670
872
|
connection.host ?? "127.0.0.1",
|
|
671
873
|
"-p",
|
|
672
|
-
String(connection.port ??
|
|
874
|
+
String(connection.port ?? defaultDatabasePort("postgres")),
|
|
673
875
|
"-U",
|
|
674
876
|
connection.user ?? "postgres",
|
|
675
877
|
"-d",
|
|
@@ -678,12 +880,21 @@ function connectionArgs(type, connection) {
|
|
|
678
880
|
case "sqlite": return [connection.database];
|
|
679
881
|
case "impala": return [
|
|
680
882
|
"-i",
|
|
681
|
-
`${connection.host ?? "127.0.0.1"}:${connection.port ??
|
|
883
|
+
`${connection.host ?? "127.0.0.1"}:${connection.port ?? defaultDatabasePort("impala")}`,
|
|
884
|
+
"-d",
|
|
885
|
+
connection.database
|
|
886
|
+
];
|
|
887
|
+
case "sqlserver": return [
|
|
888
|
+
"-S",
|
|
889
|
+
`${connection.host ?? "127.0.0.1"},${connection.port ?? defaultDatabasePort("sqlserver")}`,
|
|
890
|
+
"-U",
|
|
891
|
+
connection.user ?? defaultDatabaseUser(type),
|
|
682
892
|
"-d",
|
|
683
893
|
connection.database
|
|
684
894
|
];
|
|
685
895
|
case "oracle":
|
|
686
896
|
case "hive": return [];
|
|
897
|
+
case "clickhouse": throw new Error("ClickHouse 使用官方 HTTP 客户端,不构造 CLI argv");
|
|
687
898
|
}
|
|
688
899
|
}
|
|
689
900
|
/** Credential environment entries per type; absent password yields an empty env. */
|
|
@@ -691,8 +902,11 @@ function credentialEnv(type, connection) {
|
|
|
691
902
|
const password = connection.password;
|
|
692
903
|
if (password === void 0) return {};
|
|
693
904
|
switch (type) {
|
|
694
|
-
case "mysql":
|
|
905
|
+
case "mysql":
|
|
906
|
+
case "doris": return { MYSQL_PWD: password };
|
|
695
907
|
case "postgres": return { PGPASSWORD: password };
|
|
908
|
+
case "sqlserver": return { SQLCMDPASSWORD: password };
|
|
909
|
+
case "clickhouse":
|
|
696
910
|
case "sqlite":
|
|
697
911
|
case "oracle":
|
|
698
912
|
case "hive":
|
|
@@ -713,13 +927,16 @@ function stdinPrefix(type, connection) {
|
|
|
713
927
|
"SET HEADING OFF",
|
|
714
928
|
"SET COLSEP '|'",
|
|
715
929
|
"SET TRIMSPOOL ON",
|
|
716
|
-
connection.user !== void 0 ? `connect ${connection.user}${connection.password !== void 0 ? `/${connection.password}` : ""}@${connection.host ?? "127.0.0.1"}:${connection.port ??
|
|
930
|
+
connection.user !== void 0 ? `connect ${connection.user}${connection.password !== void 0 ? `/${connection.password}` : ""}@${connection.host ?? "127.0.0.1"}:${connection.port ?? defaultDatabasePort("oracle")}/${connection.database}` : ""
|
|
717
931
|
].filter((line) => line !== "").join("\n")}\n`;
|
|
718
|
-
case "hive": return connection.user !== void 0 ? `!connect jdbc:hive2://${connection.host ?? "127.0.0.1"}:${connection.port ??
|
|
932
|
+
case "hive": return connection.user !== void 0 ? `!connect jdbc:hive2://${connection.host ?? "127.0.0.1"}:${connection.port ?? defaultDatabasePort("hive")}/${connection.database} ${connection.user} ${connection.password ?? ""}\n` : "";
|
|
719
933
|
case "mysql":
|
|
934
|
+
case "doris":
|
|
720
935
|
case "postgres":
|
|
721
936
|
case "sqlite":
|
|
722
|
-
case "impala":
|
|
937
|
+
case "impala":
|
|
938
|
+
case "clickhouse": return "";
|
|
939
|
+
case "sqlserver": return "SET NOCOUNT ON;\n";
|
|
723
940
|
}
|
|
724
941
|
}
|
|
725
942
|
/**
|
|
@@ -736,7 +953,7 @@ function structuredStdinPrefix(type, connection) {
|
|
|
736
953
|
"SET UNDERLINE OFF",
|
|
737
954
|
"SET COLSEP '|'",
|
|
738
955
|
"SET TRIMSPOOL ON",
|
|
739
|
-
connection.user !== void 0 ? `connect ${connection.user}${connection.password !== void 0 ? `/${connection.password}` : ""}@${connection.host ?? "127.0.0.1"}:${connection.port ??
|
|
956
|
+
connection.user !== void 0 ? `connect ${connection.user}${connection.password !== void 0 ? `/${connection.password}` : ""}@${connection.host ?? "127.0.0.1"}:${connection.port ?? defaultDatabasePort("oracle")}/${connection.database}` : ""
|
|
740
957
|
].filter((line) => line !== "").join("\n")}\n`;
|
|
741
958
|
}
|
|
742
959
|
/** Apply one deployment override's extra args in front of the built-in flags. */
|
|
@@ -750,6 +967,7 @@ function withOverrides(flags, override) {
|
|
|
750
967
|
* `[options] <database>`, and putting flags first is harmless for the others.
|
|
751
968
|
*/
|
|
752
969
|
function buildClientTemplate(type, connection, override) {
|
|
970
|
+
if (type === "clickhouse") throw new Error("ClickHouse 使用官方 HTTP 客户端,不构造 CLI 模板");
|
|
753
971
|
return {
|
|
754
972
|
command: override?.command ?? DEFAULT_CLIENTS_COMMAND[type],
|
|
755
973
|
args: [...withOverrides(QUERY_ARGS[type], override), ...connectionArgs(type, connection)],
|
|
@@ -759,6 +977,7 @@ function buildClientTemplate(type, connection, override) {
|
|
|
759
977
|
}
|
|
760
978
|
/** Build one client invocation for metadata runs (machine-readable flags). */
|
|
761
979
|
function buildIntrospectTemplate(type, connection, override) {
|
|
980
|
+
if (type === "clickhouse") throw new Error("ClickHouse 使用官方 HTTP 客户端,不构造 CLI 模板");
|
|
762
981
|
return {
|
|
763
982
|
command: override?.command ?? DEFAULT_CLIENTS_COMMAND[type],
|
|
764
983
|
args: [...withOverrides(INTROSPECT_ARGS[type], override), ...connectionArgs(type, connection)],
|
|
@@ -772,6 +991,7 @@ function buildIntrospectTemplate(type, connection, override) {
|
|
|
772
991
|
* tab, postgres pipe, sqlite csv, oracle pipe, hive/impala tsv).
|
|
773
992
|
*/
|
|
774
993
|
function buildStructuredQueryTemplate(type, connection, override) {
|
|
994
|
+
if (type === "clickhouse") throw new Error("ClickHouse 使用官方 HTTP 客户端,不构造 CLI 模板");
|
|
775
995
|
return {
|
|
776
996
|
command: override?.command ?? DEFAULT_CLIENTS_COMMAND[type],
|
|
777
997
|
args: [...withOverrides(STRUCTURED_QUERY_ARGS[type], override), ...connectionArgs(type, connection)],
|
|
@@ -787,12 +1007,15 @@ function buildStructuredQueryTemplate(type, connection, override) {
|
|
|
787
1007
|
*/
|
|
788
1008
|
function tableListingSql(type, connection) {
|
|
789
1009
|
switch (type) {
|
|
790
|
-
case "mysql":
|
|
1010
|
+
case "mysql":
|
|
1011
|
+
case "doris": return `SHOW TABLES FROM \`${connection?.database ?? ""}\`;`;
|
|
1012
|
+
case "clickhouse": return "SELECT name FROM system.tables WHERE database = currentDatabase() ORDER BY name;";
|
|
791
1013
|
case "postgres": return "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY 1;";
|
|
792
1014
|
case "sqlite": return "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;";
|
|
793
1015
|
case "oracle": return "SELECT table_name FROM user_tables ORDER BY 1;";
|
|
794
1016
|
case "hive":
|
|
795
1017
|
case "impala": return "SHOW TABLES;";
|
|
1018
|
+
case "sqlserver": return "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' ORDER BY TABLE_SCHEMA, TABLE_NAME;";
|
|
796
1019
|
}
|
|
797
1020
|
}
|
|
798
1021
|
/**
|
|
@@ -802,28 +1025,37 @@ function tableListingSql(type, connection) {
|
|
|
802
1025
|
function metadataQuery(kind, type, schema, table) {
|
|
803
1026
|
switch (kind) {
|
|
804
1027
|
case "schemas": switch (type) {
|
|
805
|
-
case "mysql":
|
|
1028
|
+
case "mysql":
|
|
1029
|
+
case "doris": return "SHOW DATABASES;";
|
|
1030
|
+
case "clickhouse": return "SELECT name FROM system.databases ORDER BY name;";
|
|
806
1031
|
case "postgres": return "SELECT schema_name FROM information_schema.schemata ORDER BY 1;";
|
|
807
1032
|
case "sqlite": return "SELECT 'main';";
|
|
808
1033
|
case "oracle": return "SELECT username FROM all_users ORDER BY 1;";
|
|
809
1034
|
case "hive":
|
|
810
1035
|
case "impala": return "SHOW DATABASES;";
|
|
1036
|
+
case "sqlserver": return "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA ORDER BY SCHEMA_NAME;";
|
|
811
1037
|
}
|
|
812
1038
|
case "tables": switch (type) {
|
|
813
|
-
case "mysql":
|
|
1039
|
+
case "mysql":
|
|
1040
|
+
case "doris": return `SHOW TABLES FROM ${sanitizeIdentifier(type, schema)};`;
|
|
1041
|
+
case "clickhouse": return `SELECT name FROM system.tables WHERE database=${quoteStringLiteral(schema)} ORDER BY name;`;
|
|
814
1042
|
case "postgres": return `SELECT tablename FROM pg_tables WHERE schemaname=${quoteStringLiteral(schema)} ORDER BY 1;`;
|
|
815
1043
|
case "sqlite": return "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;";
|
|
816
1044
|
case "oracle": return `SELECT table_name FROM all_tables WHERE owner=${quoteStringLiteral(schema)} ORDER BY 1;`;
|
|
817
1045
|
case "hive":
|
|
818
1046
|
case "impala": return `SHOW TABLES IN ${sanitizeIdentifier(type, schema)};`;
|
|
1047
|
+
case "sqlserver": return `SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA=${quoteStringLiteral(schema)} AND TABLE_TYPE='BASE TABLE' ORDER BY TABLE_NAME;`;
|
|
819
1048
|
}
|
|
820
1049
|
case "describe": switch (type) {
|
|
821
|
-
case "mysql":
|
|
1050
|
+
case "mysql":
|
|
1051
|
+
case "doris": return `DESCRIBE ${sanitizeIdentifier(type, schema)}.${sanitizeIdentifier(type, table)};`;
|
|
1052
|
+
case "clickhouse": return `SELECT name, type, if(startsWith(type, 'Nullable('), 'YES', 'NO') FROM system.columns WHERE database=${quoteStringLiteral(schema)} AND table=${quoteStringLiteral(table)} ORDER BY position;`;
|
|
822
1053
|
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;`;
|
|
823
1054
|
case "sqlite": return `PRAGMA table_info(${sanitizeIdentifier(type, table)});`;
|
|
824
1055
|
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;`;
|
|
825
1056
|
case "hive":
|
|
826
1057
|
case "impala": return `DESCRIBE ${sanitizeIdentifier(type, schema)}.${sanitizeIdentifier(type, table)};`;
|
|
1058
|
+
case "sqlserver": 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;`;
|
|
827
1059
|
}
|
|
828
1060
|
}
|
|
829
1061
|
}
|
|
@@ -834,8 +1066,8 @@ function metadataQuery(kind, type, schema, table) {
|
|
|
834
1066
|
* hive/impala batch modes print none (skip 0).
|
|
835
1067
|
*/
|
|
836
1068
|
function parseListing(type, stdout) {
|
|
837
|
-
const lines = stdout.split("\n");
|
|
838
|
-
const start = type === "mysql" ? 1 : 0;
|
|
1069
|
+
const lines = (type === "sqlserver" ? stripSqlServerRowCountFooter(stdout) : stdout).split("\n");
|
|
1070
|
+
const start = type === "mysql" || type === "doris" ? 1 : 0;
|
|
839
1071
|
const items = [];
|
|
840
1072
|
for (let index = start; index < lines.length; index += 1) {
|
|
841
1073
|
const name = lines[index].trim();
|
|
@@ -847,6 +1079,18 @@ function parseListing(type, stdout) {
|
|
|
847
1079
|
function parseTableListing(type, stdout) {
|
|
848
1080
|
return parseListing(type, stdout);
|
|
849
1081
|
}
|
|
1082
|
+
const SQLSERVER_ROW_COUNT_FOOTER = /^\((?:\d+\s+rows?\s+affected|(?:共)?影响(?:了)?\s*\d+\s*行|\d+\s*行受(?:到)?影响)\)$/i;
|
|
1083
|
+
/** Remove only terminal sqlcmd row-count footer lines, never matching data in the middle. */
|
|
1084
|
+
function stripSqlServerRowCountFooter(stdout) {
|
|
1085
|
+
const newline = stdout.includes("\r\n") ? "\r\n" : "\n";
|
|
1086
|
+
const lines = stdout.replace(/\r\n?/g, "\n").split("\n");
|
|
1087
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
1088
|
+
while (lines.length > 0 && SQLSERVER_ROW_COUNT_FOOTER.test(lines[lines.length - 1].trim())) {
|
|
1089
|
+
lines.pop();
|
|
1090
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
1091
|
+
}
|
|
1092
|
+
return lines.join(newline);
|
|
1093
|
+
}
|
|
850
1094
|
/**
|
|
851
1095
|
* Parse one type's describe output into columns. Formats:
|
|
852
1096
|
* - mysql `--batch`: `Field\tType\tNull\tKey\t...` (skip header);
|
|
@@ -856,13 +1100,13 @@ function parseTableListing(type, stdout) {
|
|
|
856
1100
|
* - hive/impala batch: `name\ttype\tcomment`.
|
|
857
1101
|
*/
|
|
858
1102
|
function parseColumns(type, stdout) {
|
|
859
|
-
const lines = stdout.split(
|
|
860
|
-
const start = type === "mysql" ? 1 : 0;
|
|
1103
|
+
const lines = (type === "sqlserver" ? stripSqlServerRowCountFooter(stdout) : stdout).split(/\r?\n/);
|
|
1104
|
+
const start = type === "mysql" || type === "doris" ? 1 : 0;
|
|
861
1105
|
const columns = [];
|
|
862
1106
|
for (let index = start; index < lines.length; index += 1) {
|
|
863
1107
|
const line = lines[index].trim();
|
|
864
1108
|
if (line.length === 0) continue;
|
|
865
|
-
const parts = line.includes(" ") ? line.split(" ") : line.split("|");
|
|
1109
|
+
const parts = type === "sqlserver" ? line.split("") : line.includes(" ") ? line.split(" ") : line.split("|");
|
|
866
1110
|
const nameIndex = type === "sqlite" ? 1 : 0;
|
|
867
1111
|
const name = parts[nameIndex]?.trim() ?? "";
|
|
868
1112
|
const columnType = parts[nameIndex + 1]?.trim() ?? "";
|
|
@@ -871,6 +1115,10 @@ function parseColumns(type, stdout) {
|
|
|
871
1115
|
let nullable;
|
|
872
1116
|
switch (type) {
|
|
873
1117
|
case "mysql":
|
|
1118
|
+
case "doris":
|
|
1119
|
+
nullable = rawNullable === "yes";
|
|
1120
|
+
break;
|
|
1121
|
+
case "clickhouse":
|
|
874
1122
|
nullable = rawNullable === "yes";
|
|
875
1123
|
break;
|
|
876
1124
|
case "postgres":
|
|
@@ -882,6 +1130,9 @@ function parseColumns(type, stdout) {
|
|
|
882
1130
|
case "oracle":
|
|
883
1131
|
nullable = rawNullable === "y";
|
|
884
1132
|
break;
|
|
1133
|
+
case "sqlserver":
|
|
1134
|
+
nullable = rawNullable === "yes";
|
|
1135
|
+
break;
|
|
885
1136
|
case "hive":
|
|
886
1137
|
case "impala": nullable = void 0;
|
|
887
1138
|
}
|
|
@@ -923,7 +1174,9 @@ const HOME_ENV_BY_TYPE = {
|
|
|
923
1174
|
sqlite: ["SQLITE_HOME"],
|
|
924
1175
|
oracle: ["ORACLE_HOME"],
|
|
925
1176
|
hive: ["HIVE_HOME"],
|
|
926
|
-
impala: ["IMPALA_HOME"]
|
|
1177
|
+
impala: ["IMPALA_HOME"],
|
|
1178
|
+
doris: ["MYSQL_HOME"],
|
|
1179
|
+
sqlserver: ["SQLCMD_HOME", "MSSQL_TOOLS_HOME"]
|
|
927
1180
|
};
|
|
928
1181
|
function pathApi(platform) {
|
|
929
1182
|
return platform === "win32" ? win32 : posix;
|
|
@@ -980,7 +1233,20 @@ function macFixedDirectories(type) {
|
|
|
980
1233
|
sqlite: ["/opt/homebrew/opt/sqlite/bin", "/usr/local/opt/sqlite/bin"],
|
|
981
1234
|
oracle: [],
|
|
982
1235
|
hive: ["/opt/homebrew/opt/hive/bin", "/usr/local/opt/hive/bin"],
|
|
983
|
-
impala: ["/opt/homebrew/opt/impala/bin", "/usr/local/opt/impala/bin"]
|
|
1236
|
+
impala: ["/opt/homebrew/opt/impala/bin", "/usr/local/opt/impala/bin"],
|
|
1237
|
+
doris: [
|
|
1238
|
+
"/opt/homebrew/opt/mysql-client/bin",
|
|
1239
|
+
"/opt/homebrew/opt/mysql/bin",
|
|
1240
|
+
"/usr/local/opt/mysql-client/bin",
|
|
1241
|
+
"/usr/local/opt/mysql/bin",
|
|
1242
|
+
"/usr/local/mysql/bin"
|
|
1243
|
+
],
|
|
1244
|
+
sqlserver: [
|
|
1245
|
+
"/opt/homebrew/opt/mssql-tools18/bin",
|
|
1246
|
+
"/usr/local/opt/mssql-tools18/bin",
|
|
1247
|
+
"/opt/mssql-tools18/bin",
|
|
1248
|
+
"/opt/mssql-tools/bin"
|
|
1249
|
+
]
|
|
984
1250
|
}[type],
|
|
985
1251
|
"/usr/local/bin",
|
|
986
1252
|
"/opt/local/bin",
|
|
@@ -1011,7 +1277,9 @@ function windowsFixedDirectories(type, system, paths) {
|
|
|
1011
1277
|
sqlite: [paths.join("C:\\", "sqlite"), paths.join(programFiles, "SQLite")],
|
|
1012
1278
|
oracle: [],
|
|
1013
1279
|
hive: [],
|
|
1014
|
-
impala: []
|
|
1280
|
+
impala: [],
|
|
1281
|
+
doris: [],
|
|
1282
|
+
sqlserver: [paths.join(programFiles, "Microsoft SQL Server", "Client SDK", "ODBC", "180", "Tools", "Binn"), paths.join(programFiles, "Microsoft SQL Server", "Client SDK", "ODBC", "170", "Tools", "Binn")]
|
|
1015
1283
|
};
|
|
1016
1284
|
return [
|
|
1017
1285
|
...localAppData === void 0 ? [] : [paths.join(localAppData, "Microsoft", "WinGet", "Links")],
|
|
@@ -1029,6 +1297,8 @@ function formulaPattern(type) {
|
|
|
1029
1297
|
case "oracle": return /^(?:oracle|instantclient)(?:@.+)?$/i;
|
|
1030
1298
|
case "hive": return /^hive(?:@.+)?$/i;
|
|
1031
1299
|
case "impala": return /^impala(?:@.+)?$/i;
|
|
1300
|
+
case "doris": return /^(?:mysql|mysql-client)(?:@.+)?$/i;
|
|
1301
|
+
case "sqlserver": return /^(?:mssql-tools|mssql-tools18)(?:@.+)?$/i;
|
|
1032
1302
|
}
|
|
1033
1303
|
}
|
|
1034
1304
|
function dynamicDirectories(type, system, paths) {
|
|
@@ -1071,7 +1341,7 @@ function dynamicDirectories(type, system, paths) {
|
|
|
1071
1341
|
});
|
|
1072
1342
|
} else if (system.platform === "win32") {
|
|
1073
1343
|
const roots = [environmentValue(system.env, "ProgramFiles", system.platform) ?? "C:\\Program Files", environmentValue(system.env, "ProgramFiles(x86)", system.platform) ?? "C:\\Program Files (x86)"];
|
|
1074
|
-
for (const root of roots) if (type === "mysql") result.push({
|
|
1344
|
+
for (const root of roots) if (type === "mysql" || type === "doris") result.push({
|
|
1075
1345
|
root: paths.join(root, "MySQL"),
|
|
1076
1346
|
accepts: () => true,
|
|
1077
1347
|
suffix: ["bin"]
|
|
@@ -1090,6 +1360,11 @@ function dynamicDirectories(type, system, paths) {
|
|
|
1090
1360
|
accepts: () => true,
|
|
1091
1361
|
suffix: ["bin"]
|
|
1092
1362
|
});
|
|
1363
|
+
else if (type === "sqlserver") result.push({
|
|
1364
|
+
root: paths.join(root, "Microsoft SQL Server", "Client SDK", "ODBC"),
|
|
1365
|
+
accepts: () => true,
|
|
1366
|
+
suffix: ["Tools", "Binn"]
|
|
1367
|
+
});
|
|
1093
1368
|
}
|
|
1094
1369
|
return result;
|
|
1095
1370
|
}
|
|
@@ -1192,10 +1467,85 @@ function readCaptured(reader) {
|
|
|
1192
1467
|
truncated: read.lossy
|
|
1193
1468
|
};
|
|
1194
1469
|
}
|
|
1470
|
+
/** ClickHouse endpoint construction never embeds username or password. */
|
|
1471
|
+
function clickHouseConnectionUrl(connection) {
|
|
1472
|
+
const secure = connection.secure === true;
|
|
1473
|
+
const url = new URL(`${secure ? "https" : "http"}://127.0.0.1`);
|
|
1474
|
+
url.hostname = connection.host ?? "127.0.0.1";
|
|
1475
|
+
url.port = String(connection.port ?? defaultDatabasePort("clickhouse", secure));
|
|
1476
|
+
return url.toString();
|
|
1477
|
+
}
|
|
1478
|
+
async function collectClickHouseStream(stream, maxBytes, signal) {
|
|
1479
|
+
const chunks = [];
|
|
1480
|
+
let size = 0;
|
|
1481
|
+
let truncated = false;
|
|
1482
|
+
for await (const chunk of stream) {
|
|
1483
|
+
signal.throwIfAborted();
|
|
1484
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1485
|
+
const remaining = maxBytes - size;
|
|
1486
|
+
if (remaining <= 0) {
|
|
1487
|
+
truncated = true;
|
|
1488
|
+
stream.destroy?.();
|
|
1489
|
+
break;
|
|
1490
|
+
}
|
|
1491
|
+
if (buffer.byteLength > remaining) {
|
|
1492
|
+
chunks.push(buffer.subarray(0, remaining));
|
|
1493
|
+
size += remaining;
|
|
1494
|
+
truncated = true;
|
|
1495
|
+
stream.destroy?.();
|
|
1496
|
+
break;
|
|
1497
|
+
}
|
|
1498
|
+
chunks.push(buffer);
|
|
1499
|
+
size += buffer.byteLength;
|
|
1500
|
+
}
|
|
1501
|
+
return {
|
|
1502
|
+
text: Buffer.concat(chunks, size).toString("utf8"),
|
|
1503
|
+
truncated
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
async function runClickHouseQuery(connection, sql, options, signal) {
|
|
1507
|
+
const client = createClient({
|
|
1508
|
+
url: clickHouseConnectionUrl(connection),
|
|
1509
|
+
username: connection.user ?? defaultDatabaseUser("clickhouse"),
|
|
1510
|
+
password: connection.password ?? "",
|
|
1511
|
+
database: connection.database,
|
|
1512
|
+
request_timeout: options.timeoutMs
|
|
1513
|
+
});
|
|
1514
|
+
try {
|
|
1515
|
+
if (classifyStatement(sql, "clickhouse") !== "read") {
|
|
1516
|
+
await client.command({
|
|
1517
|
+
query: sql,
|
|
1518
|
+
abort_signal: signal,
|
|
1519
|
+
clickhouse_settings: { wait_end_of_query: 1 }
|
|
1520
|
+
});
|
|
1521
|
+
return {
|
|
1522
|
+
exitCode: 0,
|
|
1523
|
+
stdout: "",
|
|
1524
|
+
stderr: "",
|
|
1525
|
+
truncated: false
|
|
1526
|
+
};
|
|
1527
|
+
}
|
|
1528
|
+
const format = options.mode === "structured" ? "JSONCompactEachRowWithNamesAndTypes" : options.mode === "introspect" ? "TabSeparated" : "TabSeparatedWithNames";
|
|
1529
|
+
const { stream } = await client.exec({
|
|
1530
|
+
query: sql,
|
|
1531
|
+
abort_signal: signal,
|
|
1532
|
+
clickhouse_settings: { default_format: format }
|
|
1533
|
+
});
|
|
1534
|
+
const stdout = await collectClickHouseStream(stream, options.maxResultChars, signal);
|
|
1535
|
+
return {
|
|
1536
|
+
exitCode: 0,
|
|
1537
|
+
stdout: stdout.text,
|
|
1538
|
+
stderr: "",
|
|
1539
|
+
truncated: stdout.truncated
|
|
1540
|
+
};
|
|
1541
|
+
} finally {
|
|
1542
|
+
await client.close();
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1195
1545
|
/**
|
|
1196
|
-
* Run one SQL text through the type's
|
|
1197
|
-
* child
|
|
1198
|
-
*
|
|
1546
|
+
* Run one SQL text through the type's shared adapter. CLI SQL is written to
|
|
1547
|
+
* child stdin (`{ data }` batch disposition), while ClickHouse SQL is an HTTP
|
|
1548
|
+
* request body; neither path puts SQL or credentials in argv.
|
|
1199
1549
|
*
|
|
1200
1550
|
* Failure classification:
|
|
1201
1551
|
* - the caller's external signal (e.g. the tool exec signal) aborts → the
|
|
@@ -1213,7 +1563,6 @@ function readCaptured(reader) {
|
|
|
1213
1563
|
* @returns the captured outcome.
|
|
1214
1564
|
*/
|
|
1215
1565
|
async function runClientQuery(ctx, connection, sql, options, externalSignal, introspect = false) {
|
|
1216
|
-
const template = options.mode === "structured" ? buildStructuredQueryTemplate(connection.type, connection, options.clients[connection.type]) : options.mode === "introspect" || introspect ? buildIntrospectTemplate(connection.type, connection, options.clients[connection.type]) : buildClientTemplate(connection.type, connection, options.clients[connection.type]);
|
|
1217
1566
|
const controller = new AbortController();
|
|
1218
1567
|
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error(`查询超过 ${options.timeoutMs}ms 未完成,已终止客户端进程`)), options.timeoutMs);
|
|
1219
1568
|
const onExternalAbort = () => {
|
|
@@ -1222,6 +1571,12 @@ async function runClientQuery(ctx, connection, sql, options, externalSignal, int
|
|
|
1222
1571
|
if (externalSignal.aborted) controller.abort(externalSignal.reason);
|
|
1223
1572
|
else externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
1224
1573
|
try {
|
|
1574
|
+
if (connection.type === "clickhouse") return await runClickHouseQuery(connection, sql, {
|
|
1575
|
+
...options,
|
|
1576
|
+
mode: options.mode ?? (introspect ? "introspect" : "query")
|
|
1577
|
+
}, controller.signal);
|
|
1578
|
+
if (connection.type === "sqlserver") assertSqlServerSafeInput(sql);
|
|
1579
|
+
const template = options.mode === "structured" ? buildStructuredQueryTemplate(connection.type, connection, options.clients[connection.type]) : options.mode === "introspect" || introspect ? buildIntrospectTemplate(connection.type, connection, options.clients[connection.type]) : buildClientTemplate(connection.type, connection, options.clients[connection.type]);
|
|
1225
1580
|
const resolution = await resolveClientExecutable({
|
|
1226
1581
|
type: connection.type,
|
|
1227
1582
|
command: template.command,
|
|
@@ -1254,7 +1609,7 @@ async function runClientQuery(ctx, connection, sql, options, externalSignal, int
|
|
|
1254
1609
|
const stderr = readCaptured(handle.collected.stderr);
|
|
1255
1610
|
return {
|
|
1256
1611
|
exitCode: outcome.exitCode,
|
|
1257
|
-
stdout: stdout.text,
|
|
1612
|
+
stdout: connection.type === "sqlserver" ? stripSqlServerRowCountFooter(stdout.text) : stdout.text,
|
|
1258
1613
|
stderr: stderr.text,
|
|
1259
1614
|
truncated: stdout.truncated || stderr.truncated
|
|
1260
1615
|
};
|
|
@@ -1263,6 +1618,231 @@ async function runClientQuery(ctx, connection, sql, options, externalSignal, int
|
|
|
1263
1618
|
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
1264
1619
|
}
|
|
1265
1620
|
}
|
|
1621
|
+
//#endregion
|
|
1622
|
+
//#region src/structured.ts
|
|
1623
|
+
function normalizeNewlines(text) {
|
|
1624
|
+
return text.replace(/\r\n?/g, "\n");
|
|
1625
|
+
}
|
|
1626
|
+
function splitLine(line, delimiter) {
|
|
1627
|
+
return line.split(delimiter);
|
|
1628
|
+
}
|
|
1629
|
+
/** Make column names valid unique JSON object keys. */
|
|
1630
|
+
function uniqueColumns(columns) {
|
|
1631
|
+
const used = /* @__PURE__ */ new Set();
|
|
1632
|
+
return columns.map((raw, index) => {
|
|
1633
|
+
let name = raw.trim();
|
|
1634
|
+
if (name.length === 0) name = `column_${index + 1}`;
|
|
1635
|
+
if (used.has(name)) {
|
|
1636
|
+
let suffix = 2;
|
|
1637
|
+
while (used.has(`${name}_${suffix}`)) suffix += 1;
|
|
1638
|
+
name = `${name}_${suffix}`;
|
|
1639
|
+
}
|
|
1640
|
+
used.add(name);
|
|
1641
|
+
return name;
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
function rowObject(columns, fields) {
|
|
1645
|
+
const row = {};
|
|
1646
|
+
for (let index = 0; index < columns.length; index += 1) row[columns[index]] = fields[index] ?? null;
|
|
1647
|
+
return row;
|
|
1648
|
+
}
|
|
1649
|
+
function emptyOutput() {
|
|
1650
|
+
return {
|
|
1651
|
+
columns: [],
|
|
1652
|
+
rows: [],
|
|
1653
|
+
rowLimitExceeded: false
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
function skipLeadingBlank(lines) {
|
|
1657
|
+
let index = 0;
|
|
1658
|
+
while (index < lines.length && lines[index].trim().length === 0) index += 1;
|
|
1659
|
+
return index;
|
|
1660
|
+
}
|
|
1661
|
+
/** PostgreSQL `-A` appends a `(N rows)` / `(N row)` footer after SELECT output. */
|
|
1662
|
+
function isPostgresFooter(line) {
|
|
1663
|
+
return /^\(\d+ rows?\)$/.test(line.trim());
|
|
1664
|
+
}
|
|
1665
|
+
function parseDelimited(stdout, delimiter, maxRows, skipFooter = false) {
|
|
1666
|
+
const lines = normalizeNewlines(stdout).split("\n");
|
|
1667
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1668
|
+
const headerIndex = skipLeadingBlank(lines);
|
|
1669
|
+
if (headerIndex >= lines.length) return emptyOutput();
|
|
1670
|
+
const columns = uniqueColumns(splitLine(lines[headerIndex], delimiter));
|
|
1671
|
+
const rows = [];
|
|
1672
|
+
let rowLimitExceeded = false;
|
|
1673
|
+
for (let index = headerIndex + 1; index < lines.length; index += 1) {
|
|
1674
|
+
const line = lines[index];
|
|
1675
|
+
if (skipFooter && isPostgresFooter(line)) continue;
|
|
1676
|
+
if (rows.length >= maxRows) {
|
|
1677
|
+
rowLimitExceeded = true;
|
|
1678
|
+
break;
|
|
1679
|
+
}
|
|
1680
|
+
rows.push(rowObject(columns, splitLine(line, delimiter)));
|
|
1681
|
+
}
|
|
1682
|
+
return {
|
|
1683
|
+
columns,
|
|
1684
|
+
rows,
|
|
1685
|
+
rowLimitExceeded
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
/** Minimal RFC-4180-style parser for sqlite3 `-csv` output. */
|
|
1689
|
+
function parseCsv(text) {
|
|
1690
|
+
const records = [];
|
|
1691
|
+
let record = [];
|
|
1692
|
+
let field = "";
|
|
1693
|
+
let quoted = false;
|
|
1694
|
+
let index = 0;
|
|
1695
|
+
const pushField = () => {
|
|
1696
|
+
record.push(field);
|
|
1697
|
+
field = "";
|
|
1698
|
+
};
|
|
1699
|
+
const pushRecord = () => {
|
|
1700
|
+
pushField();
|
|
1701
|
+
records.push(record);
|
|
1702
|
+
record = [];
|
|
1703
|
+
};
|
|
1704
|
+
while (index < text.length) {
|
|
1705
|
+
const char = text[index];
|
|
1706
|
+
if (quoted) {
|
|
1707
|
+
if (char === "\"") {
|
|
1708
|
+
if (text[index + 1] === "\"") {
|
|
1709
|
+
field += "\"";
|
|
1710
|
+
index += 2;
|
|
1711
|
+
continue;
|
|
1712
|
+
}
|
|
1713
|
+
quoted = false;
|
|
1714
|
+
index += 1;
|
|
1715
|
+
continue;
|
|
1716
|
+
}
|
|
1717
|
+
field += char;
|
|
1718
|
+
index += 1;
|
|
1719
|
+
continue;
|
|
1720
|
+
}
|
|
1721
|
+
if (char === "\"" && field.length === 0) {
|
|
1722
|
+
quoted = true;
|
|
1723
|
+
index += 1;
|
|
1724
|
+
continue;
|
|
1725
|
+
}
|
|
1726
|
+
if (char === ",") {
|
|
1727
|
+
pushField();
|
|
1728
|
+
index += 1;
|
|
1729
|
+
continue;
|
|
1730
|
+
}
|
|
1731
|
+
if (char === "\n") {
|
|
1732
|
+
pushRecord();
|
|
1733
|
+
index += 1;
|
|
1734
|
+
continue;
|
|
1735
|
+
}
|
|
1736
|
+
if (char === "\r") {
|
|
1737
|
+
if (text[index + 1] === "\n") index += 1;
|
|
1738
|
+
pushRecord();
|
|
1739
|
+
index += 1;
|
|
1740
|
+
continue;
|
|
1741
|
+
}
|
|
1742
|
+
field += char;
|
|
1743
|
+
index += 1;
|
|
1744
|
+
}
|
|
1745
|
+
if (field.length > 0 || record.length > 0) pushRecord();
|
|
1746
|
+
return records;
|
|
1747
|
+
}
|
|
1748
|
+
function parseCsvOutput(stdout, maxRows) {
|
|
1749
|
+
const records = parseCsv(normalizeNewlines(stdout)).filter((record) => !(record.length === 1 && record[0] === ""));
|
|
1750
|
+
if (records.length === 0) return emptyOutput();
|
|
1751
|
+
const columns = uniqueColumns(records[0]);
|
|
1752
|
+
const rows = [];
|
|
1753
|
+
let rowLimitExceeded = false;
|
|
1754
|
+
for (let index = 1; index < records.length; index += 1) {
|
|
1755
|
+
if (rows.length >= maxRows) {
|
|
1756
|
+
rowLimitExceeded = true;
|
|
1757
|
+
break;
|
|
1758
|
+
}
|
|
1759
|
+
rows.push(rowObject(columns, records[index]));
|
|
1760
|
+
}
|
|
1761
|
+
return {
|
|
1762
|
+
columns,
|
|
1763
|
+
rows,
|
|
1764
|
+
rowLimitExceeded
|
|
1765
|
+
};
|
|
1766
|
+
}
|
|
1767
|
+
function parseClickHouseOutput(stdout, maxRows) {
|
|
1768
|
+
const lines = normalizeNewlines(stdout).split("\n").filter((line) => line.trim() !== "");
|
|
1769
|
+
if (lines.length === 0) return emptyOutput();
|
|
1770
|
+
const parsed = lines.map((line) => JSON.parse(line));
|
|
1771
|
+
if (!Array.isArray(parsed[0])) throw new Error("ClickHouse结构化输出缺少列名行");
|
|
1772
|
+
const columns = uniqueColumns(parsed[0].map((value) => String(value)));
|
|
1773
|
+
const firstDataIndex = parsed.length > 1 && Array.isArray(parsed[1]) ? 2 : 1;
|
|
1774
|
+
const rows = [];
|
|
1775
|
+
let rowLimitExceeded = false;
|
|
1776
|
+
for (let index = firstDataIndex; index < parsed.length; index += 1) {
|
|
1777
|
+
if (rows.length >= maxRows) {
|
|
1778
|
+
rowLimitExceeded = true;
|
|
1779
|
+
break;
|
|
1780
|
+
}
|
|
1781
|
+
const record = parsed[index];
|
|
1782
|
+
if (!Array.isArray(record)) throw new Error("ClickHouse结构化输出包含非数组数据行");
|
|
1783
|
+
rows.push(rowObject(columns, record.map((value) => {
|
|
1784
|
+
if (value === null || value === void 0) return null;
|
|
1785
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
1786
|
+
})));
|
|
1787
|
+
}
|
|
1788
|
+
return {
|
|
1789
|
+
columns,
|
|
1790
|
+
rows,
|
|
1791
|
+
rowLimitExceeded
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
function parseSqlServerOutput(stdout, maxRows) {
|
|
1795
|
+
const lines = normalizeNewlines(stripSqlServerRowCountFooter(stdout)).split("\n");
|
|
1796
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1797
|
+
const headerIndex = skipLeadingBlank(lines);
|
|
1798
|
+
if (headerIndex >= lines.length) return emptyOutput();
|
|
1799
|
+
const columns = uniqueColumns(lines[headerIndex].split(""));
|
|
1800
|
+
let dataIndex = headerIndex + 1;
|
|
1801
|
+
const divider = lines[dataIndex]?.split("");
|
|
1802
|
+
if (divider !== void 0 && divider.length === columns.length && divider.every((field) => /^-+$/.test(field.trim()))) dataIndex += 1;
|
|
1803
|
+
const rows = [];
|
|
1804
|
+
let rowLimitExceeded = false;
|
|
1805
|
+
for (let index = dataIndex; index < lines.length; index += 1) {
|
|
1806
|
+
if (lines[index].trim() === "") continue;
|
|
1807
|
+
if (rows.length >= maxRows) {
|
|
1808
|
+
rowLimitExceeded = true;
|
|
1809
|
+
break;
|
|
1810
|
+
}
|
|
1811
|
+
const fields = lines[index].split("");
|
|
1812
|
+
const row = {};
|
|
1813
|
+
for (let column = 0; column < columns.length; column += 1) {
|
|
1814
|
+
const value = fields[column];
|
|
1815
|
+
row[columns[column]] = value === void 0 || value === "NULL" ? null : value;
|
|
1816
|
+
}
|
|
1817
|
+
rows.push(row);
|
|
1818
|
+
}
|
|
1819
|
+
return {
|
|
1820
|
+
columns,
|
|
1821
|
+
rows,
|
|
1822
|
+
rowLimitExceeded
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
/**
|
|
1826
|
+
* Parse one database type's structured-query stdout. The matching template is
|
|
1827
|
+
* `buildStructuredQueryTemplate`: mysql tab-separated with a header, postgres
|
|
1828
|
+
* pipe-separated with a header and row-count footer, sqlite CSV with a header,
|
|
1829
|
+
* oracle pipe-separated with heading on, hive/impala tsv with a header.
|
|
1830
|
+
*/
|
|
1831
|
+
function parseStructuredQueryOutput(type, stdout, maxRows) {
|
|
1832
|
+
switch (type) {
|
|
1833
|
+
case "mysql": return parseDelimited(stdout, " ", maxRows);
|
|
1834
|
+
case "doris": return parseDelimited(stdout, " ", maxRows);
|
|
1835
|
+
case "clickhouse": return parseClickHouseOutput(stdout, maxRows);
|
|
1836
|
+
case "postgres": return parseDelimited(stdout, "|", maxRows, true);
|
|
1837
|
+
case "sqlite": return parseCsvOutput(stdout, maxRows);
|
|
1838
|
+
case "oracle": return parseDelimited(stdout, "|", maxRows);
|
|
1839
|
+
case "hive":
|
|
1840
|
+
case "impala": return parseDelimited(stdout, " ", maxRows);
|
|
1841
|
+
case "sqlserver": return parseSqlServerOutput(stdout, maxRows);
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
const MYSQL_SCHEMA_PROBE_CONCURRENCY = 4;
|
|
1845
|
+
const MYSQL_DATABASE_ACCESS_DENIED = /\bERROR\s+1044\s+\(42000\)/i;
|
|
1266
1846
|
/** Build a password-stripped copy of one connection. */
|
|
1267
1847
|
function summarize(connection) {
|
|
1268
1848
|
const summary = {
|
|
@@ -1274,6 +1854,7 @@ function summarize(connection) {
|
|
|
1274
1854
|
if (connection.user !== void 0) summary.user = connection.user;
|
|
1275
1855
|
if (connection.passwordRef !== void 0) summary.passwordRef = connection.passwordRef;
|
|
1276
1856
|
if (connection.readonly !== void 0) summary.readonly = connection.readonly;
|
|
1857
|
+
if (connection.secure !== void 0) summary.secure = connection.secure;
|
|
1277
1858
|
if (connection.profileId !== void 0) summary.profileId = connection.profileId;
|
|
1278
1859
|
if (connection.name !== void 0) summary.name = connection.name;
|
|
1279
1860
|
if (connection.tables !== void 0) summary.tables = [...connection.tables];
|
|
@@ -1303,19 +1884,22 @@ function normalizeConnectionInput(input, cwd = process.cwd()) {
|
|
|
1303
1884
|
if (input.port !== void 0 && (!Number.isInteger(input.port) || input.port < 1 || input.port > 65535)) throw new Error("port 必须是 1-65535 的整数");
|
|
1304
1885
|
if (input.profileId !== void 0 && input.profileId.trim().length === 0) throw new Error("profileId 不能为空");
|
|
1305
1886
|
if (input.name !== void 0 && input.name.trim().length === 0) throw new Error("name 不能为空");
|
|
1887
|
+
if (input.secure !== void 0 && typeof input.secure !== "boolean") throw new Error("secure 必须是布尔值");
|
|
1306
1888
|
const connection = {
|
|
1307
1889
|
type: input.type,
|
|
1308
1890
|
database: input.type === "sqlite" ? resolve(cwd, input.database) : input.database,
|
|
1309
1891
|
credentialMode: input.type === "sqlite" ? "none" : input.passwordRef !== void 0 ? "reference" : input.password !== void 0 && input.password.length > 0 ? "password" : "none"
|
|
1310
1892
|
};
|
|
1311
1893
|
if (input.type !== "sqlite") {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1894
|
+
connection.host = input.host !== void 0 && input.host.length > 0 ? input.host : "127.0.0.1";
|
|
1895
|
+
connection.port = input.port ?? defaultDatabasePort(input.type, input.type === "clickhouse" && input.secure === true);
|
|
1896
|
+
const user = input.user !== void 0 && input.user.length > 0 ? input.user : defaultDatabaseUser(input.type);
|
|
1897
|
+
if (user !== "") connection.user = user;
|
|
1315
1898
|
if (input.password !== void 0 && input.password.length > 0) connection.password = input.password;
|
|
1316
1899
|
if (input.passwordRef !== void 0) connection.passwordRef = input.passwordRef;
|
|
1317
1900
|
}
|
|
1318
1901
|
if (input.readonly !== void 0) connection.readonly = input.readonly;
|
|
1902
|
+
if (input.type === "clickhouse" && input.secure !== void 0) connection.secure = input.secure;
|
|
1319
1903
|
if (input.profileId !== void 0) connection.profileId = input.profileId;
|
|
1320
1904
|
if (input.name !== void 0) connection.name = input.name;
|
|
1321
1905
|
return connection;
|
|
@@ -1333,6 +1917,7 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1333
1917
|
};
|
|
1334
1918
|
const runtime = /* @__PURE__ */ new Map();
|
|
1335
1919
|
const formDrafts = /* @__PURE__ */ new Map();
|
|
1920
|
+
let latestFormInitial;
|
|
1336
1921
|
const profileConnection = (sessionId) => {
|
|
1337
1922
|
if (persistence === void 0) return void 0;
|
|
1338
1923
|
const binding = persistence.getBinding(sessionId);
|
|
@@ -1365,15 +1950,15 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1365
1950
|
tables: copyTables(connection.tables)
|
|
1366
1951
|
};
|
|
1367
1952
|
};
|
|
1368
|
-
const queryOptions = (mode, connect = false) => ({
|
|
1953
|
+
const queryOptions = (mode, connect = false, maxResultChars = resolvedOptions.maxResultChars) => ({
|
|
1369
1954
|
clients: resolvedOptions.clients,
|
|
1370
1955
|
timeoutMs: connect ? resolvedOptions.connectTimeoutMs : resolvedOptions.queryTimeoutMs,
|
|
1371
|
-
maxResultChars
|
|
1956
|
+
maxResultChars,
|
|
1372
1957
|
...mode !== void 0 ? { mode } : {}
|
|
1373
1958
|
});
|
|
1374
|
-
const run = async (connection, sql, signal, introspection = false, connect = false) => {
|
|
1959
|
+
const run = async (connection, sql, signal, introspection = false, connect = false, mode, maxResultChars) => {
|
|
1375
1960
|
try {
|
|
1376
|
-
return redactQueryResult(await runClientQuery(requireContext(), connection, sql, queryOptions(
|
|
1961
|
+
return redactQueryResult(await runClientQuery(requireContext(), connection, sql, queryOptions(mode, connect, maxResultChars), signal, introspection), connection);
|
|
1377
1962
|
} catch (error) {
|
|
1378
1963
|
const message = redactSecretText(error instanceof Error ? error.message : String(error), [connection.password]);
|
|
1379
1964
|
throw new Error(message, error instanceof Error ? { cause: error } : void 0);
|
|
@@ -1387,17 +1972,48 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1387
1972
|
}
|
|
1388
1973
|
return parseTableListing(connection.type, result.stdout).slice(0, resolvedOptions.introspectMaxTables);
|
|
1389
1974
|
};
|
|
1390
|
-
const
|
|
1975
|
+
const canAccessMySqlSchema = async (connection, schema, signal) => {
|
|
1976
|
+
if (schema === connection.database) return true;
|
|
1977
|
+
const result = await run({
|
|
1978
|
+
...connection,
|
|
1979
|
+
database: schema
|
|
1980
|
+
}, "SHOW TABLES;", signal, true);
|
|
1981
|
+
if (result.exitCode === 0) return true;
|
|
1982
|
+
const detail = result.stderr.trim() !== "" ? result.stderr.trim() : result.stdout.trim();
|
|
1983
|
+
if (MYSQL_DATABASE_ACCESS_DENIED.test(detail)) return false;
|
|
1984
|
+
throw new Error(`元数据查询失败(exit ${result.exitCode}):${detail}`);
|
|
1985
|
+
};
|
|
1986
|
+
const listAccessibleMySqlSchemas = async (connection, schemas, signal) => {
|
|
1987
|
+
const visible = [];
|
|
1988
|
+
for (let offset = 0; offset < schemas.length && visible.length < resolvedOptions.introspectMaxTables; offset += MYSQL_SCHEMA_PROBE_CONCURRENCY) {
|
|
1989
|
+
signal.throwIfAborted();
|
|
1990
|
+
const batch = schemas.slice(offset, offset + MYSQL_SCHEMA_PROBE_CONCURRENCY);
|
|
1991
|
+
const accessible = await Promise.all(batch.map((schema) => canAccessMySqlSchema(connection, schema, signal)));
|
|
1992
|
+
for (let index = 0; index < batch.length; index += 1) {
|
|
1993
|
+
if (accessible[index]) visible.push(batch[index]);
|
|
1994
|
+
if (visible.length === resolvedOptions.introspectMaxTables) return visible;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
return visible;
|
|
1998
|
+
};
|
|
1999
|
+
const persistAtomically = async (sessionId, profileId, profile, draft) => {
|
|
1391
2000
|
if (persistence === void 0) return;
|
|
1392
2001
|
const previousProfile = persistence.getProfile(profileId);
|
|
1393
2002
|
const previousBinding = persistence.getBinding(sessionId);
|
|
2003
|
+
const previousDraft = persistence.getDraft?.(sessionId);
|
|
1394
2004
|
await persistence.putProfile(profileId, profile);
|
|
1395
2005
|
try {
|
|
1396
2006
|
await persistence.putBinding(sessionId, {
|
|
1397
2007
|
profileId,
|
|
1398
2008
|
updatedAt: profile.updatedAt
|
|
1399
2009
|
});
|
|
2010
|
+
await persistence.putDraft?.(sessionId, {
|
|
2011
|
+
...draft,
|
|
2012
|
+
updatedAt: profile.updatedAt
|
|
2013
|
+
});
|
|
1400
2014
|
} catch (error) {
|
|
2015
|
+
if (previousDraft === void 0) await persistence.deleteDraft?.(sessionId);
|
|
2016
|
+
else await persistence.putDraft?.(sessionId, previousDraft);
|
|
1401
2017
|
if (previousProfile === void 0) await persistence.deleteProfile(profileId);
|
|
1402
2018
|
else await persistence.putProfile(profileId, previousProfile);
|
|
1403
2019
|
if (previousBinding === void 0) await persistence.deleteBinding(sessionId);
|
|
@@ -1457,7 +2073,15 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1457
2073
|
},
|
|
1458
2074
|
getFormDraft(sessionId) {
|
|
1459
2075
|
const draft = persistence?.getDraft?.(sessionId) ?? formDrafts.get(sessionId);
|
|
1460
|
-
|
|
2076
|
+
const exactProfile = profileConnection(sessionId);
|
|
2077
|
+
if (draft !== void 0) return {
|
|
2078
|
+
...copyFormDraft(draft),
|
|
2079
|
+
...exactProfile?.passwordRef !== void 0 ? { passwordRef: exactProfile.passwordRef } : {}
|
|
2080
|
+
};
|
|
2081
|
+
if (exactProfile !== void 0) return formInitialFromConnection(exactProfile);
|
|
2082
|
+
const latestProfile = persistence?.getLatestProfile?.();
|
|
2083
|
+
if (latestProfile !== void 0) return formInitialFromConnection(connectionFromProfile(latestProfile.profileId, latestProfile.profile));
|
|
2084
|
+
return latestFormInitial === void 0 ? void 0 : copyFormInitial(latestFormInitial);
|
|
1461
2085
|
},
|
|
1462
2086
|
async saveFormDraft(sessionId, draft) {
|
|
1463
2087
|
if (sessionId.length === 0) throw new Error("sessionId 必须是非空字符串");
|
|
@@ -1480,7 +2104,10 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1480
2104
|
const tables = await verify(execution, signal, true);
|
|
1481
2105
|
const profileId = normalized.profileId ?? `session:${sessionId}`;
|
|
1482
2106
|
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1483
|
-
|
|
2107
|
+
const draft = formDraftFromConnection(normalized);
|
|
2108
|
+
await persistAtomically(sessionId, profileId, profileFromConnection(normalized, updatedAt), draft);
|
|
2109
|
+
if (persistence === void 0) formDrafts.set(sessionId, draft);
|
|
2110
|
+
latestFormInitial = formInitialFromConnection(normalized);
|
|
1484
2111
|
const published = {
|
|
1485
2112
|
...normalized,
|
|
1486
2113
|
profileId,
|
|
@@ -1517,7 +2144,9 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1517
2144
|
async listSchemas(sessionId, signal) {
|
|
1518
2145
|
const connection = await service.resolveForExecution(sessionId);
|
|
1519
2146
|
const stdout = await runMetadata(connection, "schemas", signal);
|
|
1520
|
-
|
|
2147
|
+
const schemas = parseListing(connection.type, stdout);
|
|
2148
|
+
if (connection.type === "mysql" || connection.type === "doris") return listAccessibleMySqlSchemas(connection, schemas, signal);
|
|
2149
|
+
return schemas.slice(0, resolvedOptions.introspectMaxTables);
|
|
1521
2150
|
},
|
|
1522
2151
|
async listTables(sessionId, schema, signal) {
|
|
1523
2152
|
const connection = await service.resolveForExecution(sessionId);
|
|
@@ -1540,6 +2169,36 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1540
2169
|
const connection = await service.resolveForExecution(sessionId);
|
|
1541
2170
|
if ((connection.readonly ?? resolvedOptions.readonly) && classifyStatement(sql, connection.type) === "write") throw new Error("当前连接为只读模式,拒绝执行非读语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/PRAGMA 等)");
|
|
1542
2171
|
return run(connection, sql, signal);
|
|
2172
|
+
},
|
|
2173
|
+
async executeInteractive(sessionId, sql, signal) {
|
|
2174
|
+
if (sql.trim().length === 0) throw new Error("sql 必须是非空字符串");
|
|
2175
|
+
const maxQueryChars = resolvedOptions.maxQueryChars ?? 65536;
|
|
2176
|
+
if (sql.length > maxQueryChars) throw new Error(`sql 超过长度上限(${maxQueryChars} 字符)`);
|
|
2177
|
+
assertSingleStatement(sql, "/query");
|
|
2178
|
+
const connection = await service.resolveForExecution(sessionId);
|
|
2179
|
+
const statementKind = classifyStatement(sql, connection.type);
|
|
2180
|
+
if ((connection.readonly ?? resolvedOptions.readonly) && statementKind === "write") throw new Error("当前连接为只读模式,拒绝执行非读语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/PRAGMA 等)");
|
|
2181
|
+
if (statementKind === "write") return {
|
|
2182
|
+
kind: "message",
|
|
2183
|
+
...await run(connection, sql, signal)
|
|
2184
|
+
};
|
|
2185
|
+
const limitedSql = enforceReadRowLimit(sql, connection.type, WORKBENCH_MAX_EXPORT_ROWS + 1);
|
|
2186
|
+
const startedAt = Date.now();
|
|
2187
|
+
const result = await run(connection, limitedSql, signal, false, false, "structured", WORKBENCH_MAX_RESULT_CHARS);
|
|
2188
|
+
if (result.exitCode !== 0) return {
|
|
2189
|
+
kind: "message",
|
|
2190
|
+
...result
|
|
2191
|
+
};
|
|
2192
|
+
if (result.truncated) throw new Error("查询结果超过 Web 工作台大小上限,请减少返回列或缩小字段后重试");
|
|
2193
|
+
const parsed = parseStructuredQueryOutput(connection.type, result.stdout, WORKBENCH_MAX_EXPORT_ROWS);
|
|
2194
|
+
return {
|
|
2195
|
+
kind: "table",
|
|
2196
|
+
columns: parsed.columns,
|
|
2197
|
+
rows: parsed.rows,
|
|
2198
|
+
elapsedMs: Date.now() - startedAt,
|
|
2199
|
+
truncated: parsed.rowLimitExceeded,
|
|
2200
|
+
maxRows: WORKBENCH_MAX_EXPORT_ROWS
|
|
2201
|
+
};
|
|
1543
2202
|
}
|
|
1544
2203
|
};
|
|
1545
2204
|
async function runMetadata(connection, kind, signal, schema, table) {
|
|
@@ -1557,7 +2216,7 @@ function copyTables(tables) {
|
|
|
1557
2216
|
}
|
|
1558
2217
|
function normalizeFormDraft(draft) {
|
|
1559
2218
|
if (!isDatabaseType(draft.type)) throw new Error("数据库类型无效");
|
|
1560
|
-
if (typeof draft.host !== "string" || typeof draft.port !== "string" || typeof draft.user !== "string" || typeof draft.database !== "string" || typeof draft.readonly !== "boolean") throw new Error("数据库表单草稿无效");
|
|
2219
|
+
if (typeof draft.host !== "string" || typeof draft.port !== "string" || typeof draft.user !== "string" || typeof draft.database !== "string" || typeof draft.readonly !== "boolean" || draft.secure !== void 0 && typeof draft.secure !== "boolean") throw new Error("数据库表单草稿无效");
|
|
1561
2220
|
return copyFormDraft(draft);
|
|
1562
2221
|
}
|
|
1563
2222
|
function copyFormDraft(draft) {
|
|
@@ -1567,11 +2226,32 @@ function copyFormDraft(draft) {
|
|
|
1567
2226
|
port: draft.port,
|
|
1568
2227
|
user: draft.user,
|
|
1569
2228
|
database: draft.database,
|
|
1570
|
-
readonly: draft.readonly
|
|
2229
|
+
readonly: draft.readonly,
|
|
2230
|
+
...draft.type === "clickhouse" ? { secure: draft.secure ?? false } : {}
|
|
2231
|
+
};
|
|
2232
|
+
}
|
|
2233
|
+
function copyFormInitial(initial) {
|
|
2234
|
+
return {
|
|
2235
|
+
...copyFormDraft(initial),
|
|
2236
|
+
...initial.passwordRef !== void 0 ? { passwordRef: initial.passwordRef } : {}
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
function formDraftFromConnection(connection) {
|
|
2240
|
+
return {
|
|
2241
|
+
type: connection.type,
|
|
2242
|
+
host: connection.type === "sqlite" ? "" : connection.host ?? "",
|
|
2243
|
+
port: connection.type === "sqlite" || connection.port === void 0 ? "" : String(connection.port),
|
|
2244
|
+
user: connection.type === "sqlite" ? "" : connection.user ?? "",
|
|
2245
|
+
database: connection.database,
|
|
2246
|
+
readonly: connection.readonly ?? false,
|
|
2247
|
+
...connection.type === "clickhouse" ? { secure: connection.secure ?? false } : {}
|
|
1571
2248
|
};
|
|
1572
2249
|
}
|
|
1573
|
-
function
|
|
1574
|
-
return
|
|
2250
|
+
function formInitialFromConnection(connection) {
|
|
2251
|
+
return {
|
|
2252
|
+
...formDraftFromConnection(connection),
|
|
2253
|
+
...connection.passwordRef !== void 0 ? { passwordRef: connection.passwordRef } : {}
|
|
2254
|
+
};
|
|
1575
2255
|
}
|
|
1576
2256
|
function validatePasswordRef(value) {
|
|
1577
2257
|
try {
|
|
@@ -1594,6 +2274,7 @@ function connectionFromProfile(profileId, profile) {
|
|
|
1594
2274
|
...profile.port !== void 0 ? { port: profile.port } : {},
|
|
1595
2275
|
...profile.user !== void 0 ? { user: profile.user } : {},
|
|
1596
2276
|
...profile.readonly !== void 0 ? { readonly: profile.readonly } : {},
|
|
2277
|
+
...profile.secure !== void 0 ? { secure: profile.secure } : {},
|
|
1597
2278
|
...profile.passwordRef !== void 0 ? { passwordRef: profile.passwordRef } : {},
|
|
1598
2279
|
credentialMode: profile.credentialMode ?? (profile.type === "sqlite" ? "none" : profile.passwordRef !== void 0 ? "reference" : "password")
|
|
1599
2280
|
};
|
|
@@ -1608,6 +2289,7 @@ function profileFromConnection(connection, updatedAt) {
|
|
|
1608
2289
|
...connection.port !== void 0 ? { port: connection.port } : {},
|
|
1609
2290
|
...connection.user !== void 0 ? { user: connection.user } : {},
|
|
1610
2291
|
...connection.readonly !== void 0 ? { readonly: connection.readonly } : {},
|
|
2292
|
+
...connection.secure !== void 0 ? { secure: connection.secure } : {},
|
|
1611
2293
|
...connection.passwordRef !== void 0 ? { passwordRef: connection.passwordRef } : {},
|
|
1612
2294
|
...connection.credentialMode !== void 0 ? { credentialMode: connection.credentialMode } : {}
|
|
1613
2295
|
};
|
|
@@ -1626,4 +2308,4 @@ function requireIdentifier(type, value, label) {
|
|
|
1626
2308
|
return value;
|
|
1627
2309
|
}
|
|
1628
2310
|
//#endregion
|
|
1629
|
-
export {
|
|
2311
|
+
export { parseStructuredQueryOutput as a, clientsSchema as c, validatePasswordRef as i, enforceReadRowLimit as l, redactQueryResult as n, runClientQuery as o, redactSecretText as r, classifyStatement as s, createConnectionService as t, assertSingleStatement as u };
|