@yejiming/dsh-data-agent 0.0.12 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +97 -16
- package/README.md +97 -16
- 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 +100 -0
- package/conformance/dsh-ecosystem/restrictions.json +20 -0
- package/cordis.patch.yml +6 -3
- package/dsh-plugin.json +72 -0
- package/lib/catalog-DEJqOXRo.js +1944 -0
- package/lib/catalog-identity-CVftmvQL.js +96 -0
- package/lib/client.js +3465 -138
- package/lib/client.js.map +1 -1
- package/lib/command-CzzSPmag.js +1719 -0
- package/lib/command.js +2 -2
- package/lib/{connections-5sfdEDsG.js → connections-CFXOZTHZ.js} +964 -68
- package/lib/ecosystem.js +19 -0
- package/lib/index.js +392 -34
- package/lib/routes.js +260 -8
- package/lib/{tool-DgL0fBfj.js → tool-DNkywSph.js} +367 -168
- package/lib/tool.js +1 -1
- package/lib/types/catalog-adapters.d.ts +52 -0
- package/lib/types/catalog-ai.d.ts +49 -0
- package/lib/types/catalog-command.d.ts +28 -0
- package/lib/types/catalog-identity.d.ts +23 -0
- package/lib/types/catalog-storage.d.ts +265 -0
- package/lib/types/catalog-tools.d.ts +5 -0
- package/lib/types/catalog-tui.d.ts +18 -0
- package/lib/types/catalog-types.d.ts +1376 -0
- package/lib/types/catalog.d.ts +59 -0
- package/lib/types/client/CatalogPanel.d.ts +15 -0
- package/lib/types/client/DataAgentWorkbench.d.ts +1 -2
- package/lib/types/client/QueryResultTable.d.ts +13 -0
- package/lib/types/client/catalog-client.d.ts +57 -0
- package/lib/types/client/locales.d.ts +290 -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 +16 -5
- package/lib/types/connections.d.ts +42 -4
- package/lib/types/database-types.d.ts +23 -0
- package/lib/types/defaults.d.ts +26 -0
- package/lib/types/ecosystem.d.ts +13 -0
- package/lib/types/index.d.ts +58 -15
- 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 +21 -12
- package/package.json +30 -4
- package/preset/data-agent/agent.cordis.yml +13 -2
- package/lib/command-DuCpwVbl.js +0 -875
- package/lib/defaults-DP4RyRh1.js +0 -21
|
@@ -1,9 +1,115 @@
|
|
|
1
|
-
import "./defaults-DP4RyRh1.js";
|
|
2
1
|
import { readdir } from "node:fs/promises";
|
|
3
2
|
import { homedir } from "node:os";
|
|
4
3
|
import { posix, resolve, win32 } from "node:path";
|
|
5
4
|
import z from "schemastery";
|
|
6
5
|
import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
6
|
+
import { createClient } from "@clickhouse/client";
|
|
7
|
+
//#region src/database-types.ts
|
|
8
|
+
/**
|
|
9
|
+
* Browser-safe database type descriptors shared by every DSH surface.
|
|
10
|
+
* Keep this module dependency-free: server-only client/process details belong
|
|
11
|
+
* in the database adapters, not in Web or persistence bundles.
|
|
12
|
+
*/
|
|
13
|
+
const DATABASE_TYPES = [
|
|
14
|
+
"mysql",
|
|
15
|
+
"postgres",
|
|
16
|
+
"sqlite",
|
|
17
|
+
"oracle",
|
|
18
|
+
"hive",
|
|
19
|
+
"impala",
|
|
20
|
+
"clickhouse",
|
|
21
|
+
"doris",
|
|
22
|
+
"sqlserver"
|
|
23
|
+
];
|
|
24
|
+
const DATABASE_TYPE_DESCRIPTORS = {
|
|
25
|
+
mysql: {
|
|
26
|
+
type: "mysql",
|
|
27
|
+
label: "MySQL",
|
|
28
|
+
localeKey: "type.mysql",
|
|
29
|
+
defaultPort: 3306,
|
|
30
|
+
defaultUser: "root",
|
|
31
|
+
fileBased: false
|
|
32
|
+
},
|
|
33
|
+
postgres: {
|
|
34
|
+
type: "postgres",
|
|
35
|
+
label: "PostgreSQL",
|
|
36
|
+
localeKey: "type.postgres",
|
|
37
|
+
defaultPort: 5432,
|
|
38
|
+
defaultUser: "postgres",
|
|
39
|
+
fileBased: false
|
|
40
|
+
},
|
|
41
|
+
sqlite: {
|
|
42
|
+
type: "sqlite",
|
|
43
|
+
label: "SQLite",
|
|
44
|
+
localeKey: "type.sqlite",
|
|
45
|
+
defaultPort: 0,
|
|
46
|
+
defaultUser: "",
|
|
47
|
+
fileBased: true
|
|
48
|
+
},
|
|
49
|
+
oracle: {
|
|
50
|
+
type: "oracle",
|
|
51
|
+
label: "Oracle",
|
|
52
|
+
localeKey: "type.oracle",
|
|
53
|
+
defaultPort: 1521,
|
|
54
|
+
defaultUser: "",
|
|
55
|
+
fileBased: false
|
|
56
|
+
},
|
|
57
|
+
hive: {
|
|
58
|
+
type: "hive",
|
|
59
|
+
label: "Hive",
|
|
60
|
+
localeKey: "type.hive",
|
|
61
|
+
defaultPort: 1e4,
|
|
62
|
+
defaultUser: "",
|
|
63
|
+
fileBased: false
|
|
64
|
+
},
|
|
65
|
+
impala: {
|
|
66
|
+
type: "impala",
|
|
67
|
+
label: "Impala",
|
|
68
|
+
localeKey: "type.impala",
|
|
69
|
+
defaultPort: 21050,
|
|
70
|
+
defaultUser: "",
|
|
71
|
+
fileBased: false
|
|
72
|
+
},
|
|
73
|
+
clickhouse: {
|
|
74
|
+
type: "clickhouse",
|
|
75
|
+
label: "ClickHouse",
|
|
76
|
+
localeKey: "type.clickhouse",
|
|
77
|
+
defaultPort: 8123,
|
|
78
|
+
securePort: 8443,
|
|
79
|
+
defaultUser: "default",
|
|
80
|
+
fileBased: false
|
|
81
|
+
},
|
|
82
|
+
doris: {
|
|
83
|
+
type: "doris",
|
|
84
|
+
label: "Apache Doris",
|
|
85
|
+
localeKey: "type.doris",
|
|
86
|
+
defaultPort: 9030,
|
|
87
|
+
defaultUser: "root",
|
|
88
|
+
fileBased: false
|
|
89
|
+
},
|
|
90
|
+
sqlserver: {
|
|
91
|
+
type: "sqlserver",
|
|
92
|
+
label: "SQL Server",
|
|
93
|
+
localeKey: "type.sqlserver",
|
|
94
|
+
defaultPort: 1433,
|
|
95
|
+
defaultUser: "sa",
|
|
96
|
+
fileBased: false
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function isDatabaseType(value) {
|
|
100
|
+
return typeof value === "string" && DATABASE_TYPES.includes(value);
|
|
101
|
+
}
|
|
102
|
+
function defaultDatabasePort(type, secure = false) {
|
|
103
|
+
const descriptor = DATABASE_TYPE_DESCRIPTORS[type];
|
|
104
|
+
return secure && descriptor.securePort !== void 0 ? descriptor.securePort : descriptor.defaultPort;
|
|
105
|
+
}
|
|
106
|
+
function defaultDatabaseUser(type) {
|
|
107
|
+
return DATABASE_TYPE_DESCRIPTORS[type].defaultUser;
|
|
108
|
+
}
|
|
109
|
+
function databaseTypeLabel(type) {
|
|
110
|
+
return DATABASE_TYPE_DESCRIPTORS[type].label;
|
|
111
|
+
}
|
|
112
|
+
//#endregion
|
|
7
113
|
//#region src/sql.ts
|
|
8
114
|
/**
|
|
9
115
|
* Lightweight SQL-text scanning helpers shared by the sql-cmd tool half and
|
|
@@ -13,7 +119,7 @@ import { credentialRef } from "@deepseek-ai/dsh-credentials";
|
|
|
13
119
|
* docs/optimization-opportunities.md:
|
|
14
120
|
*
|
|
15
121
|
* - a single tool call carries at most ONE SQL statement;
|
|
16
|
-
* - `maxRows` can be enforced with a real
|
|
122
|
+
* - `maxRows` can be enforced with a real dialect-level row bound, not just a prompt.
|
|
17
123
|
*
|
|
18
124
|
* @module @yejiming/dsh-data-agent/sql
|
|
19
125
|
*/
|
|
@@ -233,6 +339,82 @@ function hasTopLevelKeyword(sql, keyword) {
|
|
|
233
339
|
}
|
|
234
340
|
return false;
|
|
235
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Preserve executable SQL text while replacing strings, quoted identifiers,
|
|
344
|
+
* dollar/Oracle quoted bodies, and comments with spaces. Newlines are kept so
|
|
345
|
+
* line-oriented client directives can be checked without false positives.
|
|
346
|
+
*/
|
|
347
|
+
function maskSqlLiteralsAndComments(sql) {
|
|
348
|
+
const chars = sql.split("");
|
|
349
|
+
const mask = (start, end) => {
|
|
350
|
+
for (let index = start; index < end; index += 1) if (chars[index] !== "\n" && chars[index] !== "\r") chars[index] = " ";
|
|
351
|
+
};
|
|
352
|
+
let index = 0;
|
|
353
|
+
while (index < sql.length) {
|
|
354
|
+
const char = sql[index];
|
|
355
|
+
if (sql.startsWith("--", index)) {
|
|
356
|
+
const end = skipLineComment(sql, index + 2);
|
|
357
|
+
mask(index, end);
|
|
358
|
+
index = end;
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (sql.startsWith("/*", index)) {
|
|
362
|
+
const end = skipBlockComment(sql, index);
|
|
363
|
+
mask(index, end);
|
|
364
|
+
index = end;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (char === "'" || char === "\"" || char === "`") {
|
|
368
|
+
const end = skipQuoted(sql, index);
|
|
369
|
+
mask(index, end);
|
|
370
|
+
index = end;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (char === "[") {
|
|
374
|
+
let end = index + 1;
|
|
375
|
+
while (end < sql.length) {
|
|
376
|
+
if (sql[end] === "]" && sql[end + 1] === "]") {
|
|
377
|
+
end += 2;
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (sql[end] === "]") {
|
|
381
|
+
end += 1;
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
end += 1;
|
|
385
|
+
}
|
|
386
|
+
mask(index, end);
|
|
387
|
+
index = end;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
if (char === "$") {
|
|
391
|
+
const end = skipDollarQuoted(sql, index);
|
|
392
|
+
if (end !== -1) {
|
|
393
|
+
mask(index, end);
|
|
394
|
+
index = end;
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
const oracleEnd = skipOracleQuoted(sql, index);
|
|
399
|
+
if (oracleEnd !== -1) {
|
|
400
|
+
mask(index, oracleEnd);
|
|
401
|
+
index = oracleEnd;
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
index += 1;
|
|
405
|
+
}
|
|
406
|
+
return chars.join("");
|
|
407
|
+
}
|
|
408
|
+
/** Reject commands interpreted by sqlcmd itself rather than by SQL Server. */
|
|
409
|
+
function assertSqlServerSafeInput(sql, label = "SQL Server SQL") {
|
|
410
|
+
const executable = maskSqlLiteralsAndComments(sql);
|
|
411
|
+
if (/\$\([^\r\n)]*\)/.test(executable)) throw new Error(`${label}: 禁止 sqlcmd 变量替换 $(...)`);
|
|
412
|
+
for (const line of executable.split(/\r?\n/)) {
|
|
413
|
+
const command = line.trimStart();
|
|
414
|
+
if (command === "") continue;
|
|
415
|
+
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 批次分隔符与客户端脚本指令`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
236
418
|
function trailingLineCommentStart(sql, end) {
|
|
237
419
|
let index = sql.lastIndexOf("\n", end - 1) + 1;
|
|
238
420
|
while (index < end) {
|
|
@@ -449,7 +631,11 @@ function classifyStatement(sql, type) {
|
|
|
449
631
|
const rest = stripLeadingComments(sql);
|
|
450
632
|
const tokenMatch = rest.match(/^[A-Za-z_]+/);
|
|
451
633
|
if (tokenMatch === null) return "write";
|
|
452
|
-
|
|
634
|
+
const token = tokenMatch[0].toLowerCase();
|
|
635
|
+
const executable = maskSqlLiteralsAndComments(rest);
|
|
636
|
+
if (type === "sqlserver" && /\binto\b/i.test(executable)) return "write";
|
|
637
|
+
if ((type === "mysql" || type === "doris" || type === "clickhouse") && /\binto\s+(?:out|dump)file\b/i.test(executable)) return "write";
|
|
638
|
+
switch (token) {
|
|
453
639
|
case "select":
|
|
454
640
|
case "show":
|
|
455
641
|
case "describe":
|
|
@@ -478,12 +664,69 @@ function enforceReadRowLimit(sql, type, maxRows) {
|
|
|
478
664
|
if (classifyStatement(sql, type) !== "read") return sql;
|
|
479
665
|
const first = stripLeadingComments(sql).match(/^[A-Za-z_]+/)?.[0]?.toLowerCase();
|
|
480
666
|
if (first !== "select" && first !== "with") return sql;
|
|
667
|
+
if (type === "sqlserver") return enforceSqlServerRowLimit(sql, maxRows);
|
|
481
668
|
const hadTrailingSemicolon = /;\s*$/.test(sql);
|
|
482
669
|
if (!hasTopLevelKeyword(sql, "limit") && type !== "oracle") return `${stripTrailingTerminator(sql)} LIMIT ${maxRows}${hadTrailingSemicolon ? ";" : ""}`;
|
|
483
670
|
if (type === "oracle") return `SELECT * FROM (${stripTrailingTerminator(sql)}) dsh_limit WHERE ROWNUM <= ${maxRows}${hadTrailingSemicolon ? ";" : ""}`;
|
|
484
671
|
if (!hasTopLevelKeyword(sql, "limit")) return sql;
|
|
485
672
|
return rewriteTopLevelLimit(sql, maxRows);
|
|
486
673
|
}
|
|
674
|
+
function findTopLevelKeywordIndex(sql, keyword) {
|
|
675
|
+
const masked = maskSqlLiteralsAndComments(sql);
|
|
676
|
+
const needle = keyword.toLowerCase();
|
|
677
|
+
let depth = 0;
|
|
678
|
+
for (let index = 0; index < masked.length; index += 1) {
|
|
679
|
+
const char = masked[index];
|
|
680
|
+
if (char === "(") {
|
|
681
|
+
depth += 1;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (char === ")") {
|
|
685
|
+
depth = Math.max(0, depth - 1);
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (depth !== 0) continue;
|
|
689
|
+
if (masked.slice(index, index + needle.length).toLowerCase() !== needle) continue;
|
|
690
|
+
const before = index === 0 ? "" : masked[index - 1];
|
|
691
|
+
const after = masked[index + needle.length] ?? "";
|
|
692
|
+
if ((before === "" || !/[A-Za-z0-9_$]/.test(before)) && (after === "" || !/[A-Za-z0-9_$]/.test(after))) return index;
|
|
693
|
+
}
|
|
694
|
+
return -1;
|
|
695
|
+
}
|
|
696
|
+
/** Add or tighten a T-SQL row limit without ever emitting MySQL LIMIT. */
|
|
697
|
+
function enforceSqlServerRowLimit(sql, maxRows) {
|
|
698
|
+
const hadTrailingSemicolon = /;\s*$/.test(sql);
|
|
699
|
+
const body = stripTrailingTerminator(sql);
|
|
700
|
+
for (const keyword of [
|
|
701
|
+
"union",
|
|
702
|
+
"intersect",
|
|
703
|
+
"except"
|
|
704
|
+
]) if (hasTopLevelKeyword(body, keyword)) throw new Error("SQL Server compound query 无法安全自动限行,请显式包装查询并使用 TOP");
|
|
705
|
+
const selectIndex = findTopLevelKeywordIndex(body, "select");
|
|
706
|
+
if (selectIndex === -1) throw new Error("SQL Server 查询无法定位顶层 SELECT,无法安全自动限行");
|
|
707
|
+
const offsetIndex = findTopLevelKeywordIndex(body, "offset");
|
|
708
|
+
const fetchIndex = findTopLevelKeywordIndex(body, "fetch");
|
|
709
|
+
if (offsetIndex !== -1 || fetchIndex !== -1) {
|
|
710
|
+
if (offsetIndex === -1 || fetchIndex === -1 || fetchIndex < offsetIndex) throw new Error("SQL Server OFFSET/FETCH 查询无法安全自动改写,请使用完整的 OFFSET ... FETCH NEXT n ROWS ONLY");
|
|
711
|
+
const fetch = body.slice(fetchIndex).match(/^fetch\s+next\s+(\d+)\s+rows?\s+only\b/i);
|
|
712
|
+
if (fetch === null) throw new Error("SQL Server OFFSET/FETCH 查询无法安全自动改写,请显式设置数字 FETCH NEXT");
|
|
713
|
+
if (Number(fetch[1]) <= maxRows) return sql;
|
|
714
|
+
const replacement = fetch[0].replace(fetch[1], String(maxRows));
|
|
715
|
+
return `${body.slice(0, fetchIndex)}${replacement}${body.slice(fetchIndex + fetch[0].length)}${hadTrailingSemicolon ? ";" : ""}`;
|
|
716
|
+
}
|
|
717
|
+
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);
|
|
718
|
+
if (prefixMatch === null) throw new Error("SQL Server SELECT 形态无法安全自动限行");
|
|
719
|
+
const existing = prefixMatch[2] ?? prefixMatch[3];
|
|
720
|
+
if (prefixMatch[4] !== void 0 || prefixMatch[5] !== void 0) throw new Error("SQL Server TOP PERCENT/WITH TIES 无法安全自动限行,请改用显式 TOP (n)");
|
|
721
|
+
if (existing !== void 0) {
|
|
722
|
+
if (Number(existing) <= maxRows) return sql;
|
|
723
|
+
const topStart = selectIndex + 6 + prefixMatch[1].length;
|
|
724
|
+
const topLength = prefixMatch[0].length - prefixMatch[1].length;
|
|
725
|
+
return `${body.slice(0, topStart)}TOP (${maxRows}) ${body.slice(topStart + topLength)}${hadTrailingSemicolon ? ";" : ""}`;
|
|
726
|
+
}
|
|
727
|
+
const insertAt = selectIndex + 6 + prefixMatch[1].length;
|
|
728
|
+
return `${body.slice(0, insertAt)}TOP (${maxRows}) ${body.slice(insertAt)}${hadTrailingSemicolon ? ";" : ""}`;
|
|
729
|
+
}
|
|
487
730
|
/** Rewrite the first top-level `LIMIT n` / `LIMIT n, m` with a capped row count. */
|
|
488
731
|
function rewriteTopLevelLimit(sql, maxRows) {
|
|
489
732
|
let depth = 0;
|
|
@@ -576,11 +819,14 @@ function sanitizeIdentifier(type, identifier) {
|
|
|
576
819
|
if (!/^[A-Za-z0-9_$]+$/.test(identifier)) throw new Error(`标识符含非法字符(仅允许字母、数字与 _ $):${identifier}`);
|
|
577
820
|
switch (type) {
|
|
578
821
|
case "mysql":
|
|
822
|
+
case "doris":
|
|
823
|
+
case "clickhouse":
|
|
579
824
|
case "hive":
|
|
580
825
|
case "impala": return "`" + identifier.replace(/`/g, "``") + "`";
|
|
581
826
|
case "postgres":
|
|
582
827
|
case "oracle":
|
|
583
828
|
case "sqlite": return "\"" + identifier.replace(/"/g, "\"\"") + "\"";
|
|
829
|
+
case "sqlserver": return "[" + identifier.replace(/]/g, "]]") + "]";
|
|
584
830
|
}
|
|
585
831
|
}
|
|
586
832
|
/**
|
|
@@ -600,43 +846,100 @@ const clientConfigSchema = z.object({
|
|
|
600
846
|
args: z.array(z.string()),
|
|
601
847
|
searchPaths: z.array(z.string())
|
|
602
848
|
});
|
|
603
|
-
/** Loader schema for
|
|
604
|
-
const
|
|
849
|
+
/** Loader schema for CLI overrides; ClickHouse has connection-level HTTP transport instead. */
|
|
850
|
+
const cliDatabaseTypeSchema = z.union([
|
|
851
|
+
z.const("mysql"),
|
|
852
|
+
z.const("postgres"),
|
|
853
|
+
z.const("sqlite"),
|
|
854
|
+
z.const("oracle"),
|
|
855
|
+
z.const("hive"),
|
|
856
|
+
z.const("impala"),
|
|
857
|
+
z.const("doris"),
|
|
858
|
+
z.const("sqlserver")
|
|
859
|
+
]);
|
|
860
|
+
const clientsSchema = z.dict(clientConfigSchema, cliDatabaseTypeSchema).default({});
|
|
861
|
+
/**
|
|
862
|
+
* MySQL output must match the subprocess collector's UTF-8 decoder instead of
|
|
863
|
+
* inheriting a platform locale such as a legacy Windows code page.
|
|
864
|
+
*/
|
|
865
|
+
const MYSQL_COMMON_ARGS = [
|
|
866
|
+
"--default-character-set=utf8mb4",
|
|
867
|
+
"--batch",
|
|
868
|
+
"--raw"
|
|
869
|
+
];
|
|
605
870
|
/** Query-mode flag arguments per type (plain/human output). */
|
|
606
871
|
const QUERY_ARGS = {
|
|
607
|
-
mysql:
|
|
872
|
+
mysql: MYSQL_COMMON_ARGS,
|
|
873
|
+
doris: MYSQL_COMMON_ARGS,
|
|
608
874
|
postgres: ["-A"],
|
|
609
875
|
sqlite: ["-header", "-column"],
|
|
610
876
|
oracle: ["-S", "/nolog"],
|
|
611
877
|
hive: ["--silent=true", "--outputformat=tsv2"],
|
|
612
|
-
impala: ["-B"]
|
|
878
|
+
impala: ["-B"],
|
|
879
|
+
sqlserver: [
|
|
880
|
+
"-b",
|
|
881
|
+
"-V",
|
|
882
|
+
"11",
|
|
883
|
+
"-r",
|
|
884
|
+
"1",
|
|
885
|
+
"-x",
|
|
886
|
+
"-W",
|
|
887
|
+
"-w",
|
|
888
|
+
"65535",
|
|
889
|
+
"-s",
|
|
890
|
+
""
|
|
891
|
+
],
|
|
892
|
+
clickhouse: []
|
|
613
893
|
};
|
|
614
894
|
/** Introspection-mode flag arguments per type (machine-readable listing). */
|
|
615
895
|
const INTROSPECT_ARGS = {
|
|
616
|
-
mysql:
|
|
896
|
+
mysql: MYSQL_COMMON_ARGS,
|
|
897
|
+
doris: MYSQL_COMMON_ARGS,
|
|
617
898
|
postgres: ["-t", "-A"],
|
|
618
899
|
sqlite: ["-noheader", "-list"],
|
|
619
900
|
oracle: ["-S", "/nolog"],
|
|
620
901
|
hive: ["--silent=true", "--outputformat=tsv2"],
|
|
621
|
-
impala: ["-B"]
|
|
902
|
+
impala: ["-B"],
|
|
903
|
+
sqlserver: [
|
|
904
|
+
"-b",
|
|
905
|
+
"-V",
|
|
906
|
+
"11",
|
|
907
|
+
"-r",
|
|
908
|
+
"1",
|
|
909
|
+
"-x",
|
|
910
|
+
"-W",
|
|
911
|
+
"-w",
|
|
912
|
+
"65535",
|
|
913
|
+
"-s",
|
|
914
|
+
"",
|
|
915
|
+
"-h",
|
|
916
|
+
"-1"
|
|
917
|
+
],
|
|
918
|
+
clickhouse: []
|
|
622
919
|
};
|
|
623
920
|
/** Structured `sql-query` flag arguments: header + one row per line. */
|
|
624
921
|
const STRUCTURED_QUERY_ARGS = {
|
|
625
|
-
mysql:
|
|
922
|
+
mysql: MYSQL_COMMON_ARGS,
|
|
923
|
+
doris: MYSQL_COMMON_ARGS,
|
|
626
924
|
postgres: ["-A"],
|
|
627
925
|
sqlite: ["-header", "-csv"],
|
|
628
926
|
oracle: ["-S", "/nolog"],
|
|
629
927
|
hive: ["--silent=true", "--outputformat=tsv2"],
|
|
630
|
-
impala: ["-B", "--print_header"]
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
928
|
+
impala: ["-B", "--print_header"],
|
|
929
|
+
sqlserver: [
|
|
930
|
+
"-b",
|
|
931
|
+
"-V",
|
|
932
|
+
"11",
|
|
933
|
+
"-r",
|
|
934
|
+
"1",
|
|
935
|
+
"-x",
|
|
936
|
+
"-W",
|
|
937
|
+
"-w",
|
|
938
|
+
"65535",
|
|
939
|
+
"-s",
|
|
940
|
+
""
|
|
941
|
+
],
|
|
942
|
+
clickhouse: []
|
|
640
943
|
};
|
|
641
944
|
/** Built-in commands per type (also the loader defaults; see `src/defaults.ts`). */
|
|
642
945
|
const DEFAULT_CLIENTS_COMMAND = {
|
|
@@ -645,7 +948,10 @@ const DEFAULT_CLIENTS_COMMAND = {
|
|
|
645
948
|
sqlite: "sqlite3",
|
|
646
949
|
oracle: "sqlplus",
|
|
647
950
|
hive: "beeline",
|
|
648
|
-
impala: "impala-shell"
|
|
951
|
+
impala: "impala-shell",
|
|
952
|
+
doris: "mysql",
|
|
953
|
+
sqlserver: "sqlcmd",
|
|
954
|
+
clickhouse: ""
|
|
649
955
|
};
|
|
650
956
|
/**
|
|
651
957
|
* Connection flags for one type. Oracle and Hive carry NO connection flags:
|
|
@@ -655,13 +961,14 @@ const DEFAULT_CLIENTS_COMMAND = {
|
|
|
655
961
|
*/
|
|
656
962
|
function connectionArgs(type, connection) {
|
|
657
963
|
switch (type) {
|
|
658
|
-
case "mysql":
|
|
964
|
+
case "mysql":
|
|
965
|
+
case "doris": return [
|
|
659
966
|
"-h",
|
|
660
967
|
connection.host ?? "127.0.0.1",
|
|
661
968
|
"-P",
|
|
662
|
-
String(connection.port ??
|
|
969
|
+
String(connection.port ?? defaultDatabasePort(type)),
|
|
663
970
|
"-u",
|
|
664
|
-
connection.user ??
|
|
971
|
+
connection.user ?? defaultDatabaseUser(type),
|
|
665
972
|
"-D",
|
|
666
973
|
connection.database
|
|
667
974
|
];
|
|
@@ -669,7 +976,7 @@ function connectionArgs(type, connection) {
|
|
|
669
976
|
"-h",
|
|
670
977
|
connection.host ?? "127.0.0.1",
|
|
671
978
|
"-p",
|
|
672
|
-
String(connection.port ??
|
|
979
|
+
String(connection.port ?? defaultDatabasePort("postgres")),
|
|
673
980
|
"-U",
|
|
674
981
|
connection.user ?? "postgres",
|
|
675
982
|
"-d",
|
|
@@ -678,12 +985,21 @@ function connectionArgs(type, connection) {
|
|
|
678
985
|
case "sqlite": return [connection.database];
|
|
679
986
|
case "impala": return [
|
|
680
987
|
"-i",
|
|
681
|
-
`${connection.host ?? "127.0.0.1"}:${connection.port ??
|
|
988
|
+
`${connection.host ?? "127.0.0.1"}:${connection.port ?? defaultDatabasePort("impala")}`,
|
|
989
|
+
"-d",
|
|
990
|
+
connection.database
|
|
991
|
+
];
|
|
992
|
+
case "sqlserver": return [
|
|
993
|
+
"-S",
|
|
994
|
+
`${connection.host ?? "127.0.0.1"},${connection.port ?? defaultDatabasePort("sqlserver")}`,
|
|
995
|
+
"-U",
|
|
996
|
+
connection.user ?? defaultDatabaseUser(type),
|
|
682
997
|
"-d",
|
|
683
998
|
connection.database
|
|
684
999
|
];
|
|
685
1000
|
case "oracle":
|
|
686
1001
|
case "hive": return [];
|
|
1002
|
+
case "clickhouse": throw new Error("ClickHouse 使用官方 HTTP 客户端,不构造 CLI argv");
|
|
687
1003
|
}
|
|
688
1004
|
}
|
|
689
1005
|
/** Credential environment entries per type; absent password yields an empty env. */
|
|
@@ -691,8 +1007,11 @@ function credentialEnv(type, connection) {
|
|
|
691
1007
|
const password = connection.password;
|
|
692
1008
|
if (password === void 0) return {};
|
|
693
1009
|
switch (type) {
|
|
694
|
-
case "mysql":
|
|
1010
|
+
case "mysql":
|
|
1011
|
+
case "doris": return { MYSQL_PWD: password };
|
|
695
1012
|
case "postgres": return { PGPASSWORD: password };
|
|
1013
|
+
case "sqlserver": return { SQLCMDPASSWORD: password };
|
|
1014
|
+
case "clickhouse":
|
|
696
1015
|
case "sqlite":
|
|
697
1016
|
case "oracle":
|
|
698
1017
|
case "hive":
|
|
@@ -713,13 +1032,16 @@ function stdinPrefix(type, connection) {
|
|
|
713
1032
|
"SET HEADING OFF",
|
|
714
1033
|
"SET COLSEP '|'",
|
|
715
1034
|
"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 ??
|
|
1035
|
+
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
1036
|
].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 ??
|
|
1037
|
+
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
1038
|
case "mysql":
|
|
1039
|
+
case "doris":
|
|
720
1040
|
case "postgres":
|
|
721
1041
|
case "sqlite":
|
|
722
|
-
case "impala":
|
|
1042
|
+
case "impala":
|
|
1043
|
+
case "clickhouse": return "";
|
|
1044
|
+
case "sqlserver": return "SET NOCOUNT ON;\n";
|
|
723
1045
|
}
|
|
724
1046
|
}
|
|
725
1047
|
/**
|
|
@@ -736,7 +1058,7 @@ function structuredStdinPrefix(type, connection) {
|
|
|
736
1058
|
"SET UNDERLINE OFF",
|
|
737
1059
|
"SET COLSEP '|'",
|
|
738
1060
|
"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 ??
|
|
1061
|
+
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
1062
|
].filter((line) => line !== "").join("\n")}\n`;
|
|
741
1063
|
}
|
|
742
1064
|
/** Apply one deployment override's extra args in front of the built-in flags. */
|
|
@@ -750,6 +1072,7 @@ function withOverrides(flags, override) {
|
|
|
750
1072
|
* `[options] <database>`, and putting flags first is harmless for the others.
|
|
751
1073
|
*/
|
|
752
1074
|
function buildClientTemplate(type, connection, override) {
|
|
1075
|
+
if (type === "clickhouse") throw new Error("ClickHouse 使用官方 HTTP 客户端,不构造 CLI 模板");
|
|
753
1076
|
return {
|
|
754
1077
|
command: override?.command ?? DEFAULT_CLIENTS_COMMAND[type],
|
|
755
1078
|
args: [...withOverrides(QUERY_ARGS[type], override), ...connectionArgs(type, connection)],
|
|
@@ -759,6 +1082,7 @@ function buildClientTemplate(type, connection, override) {
|
|
|
759
1082
|
}
|
|
760
1083
|
/** Build one client invocation for metadata runs (machine-readable flags). */
|
|
761
1084
|
function buildIntrospectTemplate(type, connection, override) {
|
|
1085
|
+
if (type === "clickhouse") throw new Error("ClickHouse 使用官方 HTTP 客户端,不构造 CLI 模板");
|
|
762
1086
|
return {
|
|
763
1087
|
command: override?.command ?? DEFAULT_CLIENTS_COMMAND[type],
|
|
764
1088
|
args: [...withOverrides(INTROSPECT_ARGS[type], override), ...connectionArgs(type, connection)],
|
|
@@ -772,6 +1096,7 @@ function buildIntrospectTemplate(type, connection, override) {
|
|
|
772
1096
|
* tab, postgres pipe, sqlite csv, oracle pipe, hive/impala tsv).
|
|
773
1097
|
*/
|
|
774
1098
|
function buildStructuredQueryTemplate(type, connection, override) {
|
|
1099
|
+
if (type === "clickhouse") throw new Error("ClickHouse 使用官方 HTTP 客户端,不构造 CLI 模板");
|
|
775
1100
|
return {
|
|
776
1101
|
command: override?.command ?? DEFAULT_CLIENTS_COMMAND[type],
|
|
777
1102
|
args: [...withOverrides(STRUCTURED_QUERY_ARGS[type], override), ...connectionArgs(type, connection)],
|
|
@@ -787,12 +1112,15 @@ function buildStructuredQueryTemplate(type, connection, override) {
|
|
|
787
1112
|
*/
|
|
788
1113
|
function tableListingSql(type, connection) {
|
|
789
1114
|
switch (type) {
|
|
790
|
-
case "mysql":
|
|
1115
|
+
case "mysql":
|
|
1116
|
+
case "doris": return `SHOW TABLES FROM \`${connection?.database ?? ""}\`;`;
|
|
1117
|
+
case "clickhouse": return "SELECT name FROM system.tables WHERE database = currentDatabase() ORDER BY name;";
|
|
791
1118
|
case "postgres": return "SELECT tablename FROM pg_tables WHERE schemaname='public' ORDER BY 1;";
|
|
792
1119
|
case "sqlite": return "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;";
|
|
793
1120
|
case "oracle": return "SELECT table_name FROM user_tables ORDER BY 1;";
|
|
794
1121
|
case "hive":
|
|
795
1122
|
case "impala": return "SHOW TABLES;";
|
|
1123
|
+
case "sqlserver": return "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE='BASE TABLE' ORDER BY TABLE_SCHEMA, TABLE_NAME;";
|
|
796
1124
|
}
|
|
797
1125
|
}
|
|
798
1126
|
/**
|
|
@@ -802,28 +1130,37 @@ function tableListingSql(type, connection) {
|
|
|
802
1130
|
function metadataQuery(kind, type, schema, table) {
|
|
803
1131
|
switch (kind) {
|
|
804
1132
|
case "schemas": switch (type) {
|
|
805
|
-
case "mysql":
|
|
1133
|
+
case "mysql":
|
|
1134
|
+
case "doris": return "SHOW DATABASES;";
|
|
1135
|
+
case "clickhouse": return "SELECT name FROM system.databases ORDER BY name;";
|
|
806
1136
|
case "postgres": return "SELECT schema_name FROM information_schema.schemata ORDER BY 1;";
|
|
807
1137
|
case "sqlite": return "SELECT 'main';";
|
|
808
1138
|
case "oracle": return "SELECT username FROM all_users ORDER BY 1;";
|
|
809
1139
|
case "hive":
|
|
810
1140
|
case "impala": return "SHOW DATABASES;";
|
|
1141
|
+
case "sqlserver": return "SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA ORDER BY SCHEMA_NAME;";
|
|
811
1142
|
}
|
|
812
1143
|
case "tables": switch (type) {
|
|
813
|
-
case "mysql":
|
|
1144
|
+
case "mysql":
|
|
1145
|
+
case "doris": return `SHOW TABLES FROM ${sanitizeIdentifier(type, schema)};`;
|
|
1146
|
+
case "clickhouse": return `SELECT name FROM system.tables WHERE database=${quoteStringLiteral(schema)} ORDER BY name;`;
|
|
814
1147
|
case "postgres": return `SELECT tablename FROM pg_tables WHERE schemaname=${quoteStringLiteral(schema)} ORDER BY 1;`;
|
|
815
1148
|
case "sqlite": return "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;";
|
|
816
1149
|
case "oracle": return `SELECT table_name FROM all_tables WHERE owner=${quoteStringLiteral(schema)} ORDER BY 1;`;
|
|
817
1150
|
case "hive":
|
|
818
1151
|
case "impala": return `SHOW TABLES IN ${sanitizeIdentifier(type, schema)};`;
|
|
1152
|
+
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
1153
|
}
|
|
820
1154
|
case "describe": switch (type) {
|
|
821
|
-
case "mysql":
|
|
1155
|
+
case "mysql":
|
|
1156
|
+
case "doris": return `DESCRIBE ${sanitizeIdentifier(type, schema)}.${sanitizeIdentifier(type, table)};`;
|
|
1157
|
+
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
1158
|
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
1159
|
case "sqlite": return `PRAGMA table_info(${sanitizeIdentifier(type, table)});`;
|
|
824
1160
|
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
1161
|
case "hive":
|
|
826
1162
|
case "impala": return `DESCRIBE ${sanitizeIdentifier(type, schema)}.${sanitizeIdentifier(type, table)};`;
|
|
1163
|
+
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
1164
|
}
|
|
828
1165
|
}
|
|
829
1166
|
}
|
|
@@ -834,8 +1171,8 @@ function metadataQuery(kind, type, schema, table) {
|
|
|
834
1171
|
* hive/impala batch modes print none (skip 0).
|
|
835
1172
|
*/
|
|
836
1173
|
function parseListing(type, stdout) {
|
|
837
|
-
const lines = stdout.split("\n");
|
|
838
|
-
const start = type === "mysql" ? 1 : 0;
|
|
1174
|
+
const lines = (type === "sqlserver" ? stripSqlServerRowCountFooter(stdout) : stdout).split("\n");
|
|
1175
|
+
const start = type === "mysql" || type === "doris" ? 1 : 0;
|
|
839
1176
|
const items = [];
|
|
840
1177
|
for (let index = start; index < lines.length; index += 1) {
|
|
841
1178
|
const name = lines[index].trim();
|
|
@@ -847,6 +1184,18 @@ function parseListing(type, stdout) {
|
|
|
847
1184
|
function parseTableListing(type, stdout) {
|
|
848
1185
|
return parseListing(type, stdout);
|
|
849
1186
|
}
|
|
1187
|
+
const SQLSERVER_ROW_COUNT_FOOTER = /^\((?:\d+\s+rows?\s+affected|(?:共)?影响(?:了)?\s*\d+\s*行|\d+\s*行受(?:到)?影响)\)$/i;
|
|
1188
|
+
/** Remove only terminal sqlcmd row-count footer lines, never matching data in the middle. */
|
|
1189
|
+
function stripSqlServerRowCountFooter(stdout) {
|
|
1190
|
+
const newline = stdout.includes("\r\n") ? "\r\n" : "\n";
|
|
1191
|
+
const lines = stdout.replace(/\r\n?/g, "\n").split("\n");
|
|
1192
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
1193
|
+
while (lines.length > 0 && SQLSERVER_ROW_COUNT_FOOTER.test(lines[lines.length - 1].trim())) {
|
|
1194
|
+
lines.pop();
|
|
1195
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
1196
|
+
}
|
|
1197
|
+
return lines.join(newline);
|
|
1198
|
+
}
|
|
850
1199
|
/**
|
|
851
1200
|
* Parse one type's describe output into columns. Formats:
|
|
852
1201
|
* - mysql `--batch`: `Field\tType\tNull\tKey\t...` (skip header);
|
|
@@ -856,13 +1205,13 @@ function parseTableListing(type, stdout) {
|
|
|
856
1205
|
* - hive/impala batch: `name\ttype\tcomment`.
|
|
857
1206
|
*/
|
|
858
1207
|
function parseColumns(type, stdout) {
|
|
859
|
-
const lines = stdout.split(
|
|
860
|
-
const start = type === "mysql" ? 1 : 0;
|
|
1208
|
+
const lines = (type === "sqlserver" ? stripSqlServerRowCountFooter(stdout) : stdout).split(/\r?\n/);
|
|
1209
|
+
const start = type === "mysql" || type === "doris" ? 1 : 0;
|
|
861
1210
|
const columns = [];
|
|
862
1211
|
for (let index = start; index < lines.length; index += 1) {
|
|
863
1212
|
const line = lines[index].trim();
|
|
864
1213
|
if (line.length === 0) continue;
|
|
865
|
-
const parts = line.includes(" ") ? line.split(" ") : line.split("|");
|
|
1214
|
+
const parts = type === "sqlserver" ? line.split("") : line.includes(" ") ? line.split(" ") : line.split("|");
|
|
866
1215
|
const nameIndex = type === "sqlite" ? 1 : 0;
|
|
867
1216
|
const name = parts[nameIndex]?.trim() ?? "";
|
|
868
1217
|
const columnType = parts[nameIndex + 1]?.trim() ?? "";
|
|
@@ -871,6 +1220,10 @@ function parseColumns(type, stdout) {
|
|
|
871
1220
|
let nullable;
|
|
872
1221
|
switch (type) {
|
|
873
1222
|
case "mysql":
|
|
1223
|
+
case "doris":
|
|
1224
|
+
nullable = rawNullable === "yes";
|
|
1225
|
+
break;
|
|
1226
|
+
case "clickhouse":
|
|
874
1227
|
nullable = rawNullable === "yes";
|
|
875
1228
|
break;
|
|
876
1229
|
case "postgres":
|
|
@@ -882,6 +1235,9 @@ function parseColumns(type, stdout) {
|
|
|
882
1235
|
case "oracle":
|
|
883
1236
|
nullable = rawNullable === "y";
|
|
884
1237
|
break;
|
|
1238
|
+
case "sqlserver":
|
|
1239
|
+
nullable = rawNullable === "yes";
|
|
1240
|
+
break;
|
|
885
1241
|
case "hive":
|
|
886
1242
|
case "impala": nullable = void 0;
|
|
887
1243
|
}
|
|
@@ -894,6 +1250,40 @@ function parseColumns(type, stdout) {
|
|
|
894
1250
|
return columns;
|
|
895
1251
|
}
|
|
896
1252
|
//#endregion
|
|
1253
|
+
//#region src/defaults.ts
|
|
1254
|
+
/**
|
|
1255
|
+
* Package-wide defaults shared by the server half (`src/index.ts`) and the
|
|
1256
|
+
* database tool half (`src/tool.ts`). Loader schemas carry these as their
|
|
1257
|
+
* defaults so a deployment may override every one of them in cordis.yml.
|
|
1258
|
+
* @module @yejiming/dsh-data-agent/defaults
|
|
1259
|
+
*/
|
|
1260
|
+
/** Preset directory name installed into `$DSH_HOME/.agent-presets/`. */
|
|
1261
|
+
const DEFAULT_PRESET_ID = "data-agent";
|
|
1262
|
+
/** End-to-end deadline for one `/connect` connectivity check, milliseconds. */
|
|
1263
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
1264
|
+
/** End-to-end deadline for one database-tool query, milliseconds. */
|
|
1265
|
+
const DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
1266
|
+
/** In-memory cap on database-tool captured output (stdout and stderr each). */
|
|
1267
|
+
const DEFAULT_MAX_RESULT_CHARS = 2e4;
|
|
1268
|
+
/** Hard row cap for one structured Web workbench result/export. */
|
|
1269
|
+
const WORKBENCH_MAX_EXPORT_ROWS = 5e4;
|
|
1270
|
+
/** Bounded capture size for the larger structured Web workbench result. */
|
|
1271
|
+
const WORKBENCH_MAX_RESULT_CHARS = 33554432;
|
|
1272
|
+
/** Cap on one /query SQL text length (abuse guard; the wire body stays small). */
|
|
1273
|
+
const DEFAULT_MAX_QUERY_CHARS = 65536;
|
|
1274
|
+
/** Catalog metadata query deadline. Kept separate from user SQL execution. */
|
|
1275
|
+
const DEFAULT_CATALOG_QUERY_TIMEOUT_MS = 3e4;
|
|
1276
|
+
/**
|
|
1277
|
+
* Per-stream capture budget for one system-catalog query. Catalog metadata is
|
|
1278
|
+
* intentionally independent from the much smaller model/interactive SQL
|
|
1279
|
+
* result budget because a schema snapshot can contain thousands of objects.
|
|
1280
|
+
*/
|
|
1281
|
+
const DEFAULT_CATALOG_MAX_RESULT_CHARS = 33554432;
|
|
1282
|
+
/** Hard bound on technical assets (including columns) staged by one run. */
|
|
1283
|
+
const DEFAULT_CATALOG_MAX_ASSETS = 5e4;
|
|
1284
|
+
/** Maximum normalized length of one database or human-authored text field. */
|
|
1285
|
+
const DEFAULT_CATALOG_MAX_TEXT_CHARS = 4096;
|
|
1286
|
+
//#endregion
|
|
897
1287
|
//#region src/client-discovery.ts
|
|
898
1288
|
/**
|
|
899
1289
|
* Cross-platform database CLI discovery.
|
|
@@ -923,7 +1313,9 @@ const HOME_ENV_BY_TYPE = {
|
|
|
923
1313
|
sqlite: ["SQLITE_HOME"],
|
|
924
1314
|
oracle: ["ORACLE_HOME"],
|
|
925
1315
|
hive: ["HIVE_HOME"],
|
|
926
|
-
impala: ["IMPALA_HOME"]
|
|
1316
|
+
impala: ["IMPALA_HOME"],
|
|
1317
|
+
doris: ["MYSQL_HOME"],
|
|
1318
|
+
sqlserver: ["SQLCMD_HOME", "MSSQL_TOOLS_HOME"]
|
|
927
1319
|
};
|
|
928
1320
|
function pathApi(platform) {
|
|
929
1321
|
return platform === "win32" ? win32 : posix;
|
|
@@ -980,7 +1372,20 @@ function macFixedDirectories(type) {
|
|
|
980
1372
|
sqlite: ["/opt/homebrew/opt/sqlite/bin", "/usr/local/opt/sqlite/bin"],
|
|
981
1373
|
oracle: [],
|
|
982
1374
|
hive: ["/opt/homebrew/opt/hive/bin", "/usr/local/opt/hive/bin"],
|
|
983
|
-
impala: ["/opt/homebrew/opt/impala/bin", "/usr/local/opt/impala/bin"]
|
|
1375
|
+
impala: ["/opt/homebrew/opt/impala/bin", "/usr/local/opt/impala/bin"],
|
|
1376
|
+
doris: [
|
|
1377
|
+
"/opt/homebrew/opt/mysql-client/bin",
|
|
1378
|
+
"/opt/homebrew/opt/mysql/bin",
|
|
1379
|
+
"/usr/local/opt/mysql-client/bin",
|
|
1380
|
+
"/usr/local/opt/mysql/bin",
|
|
1381
|
+
"/usr/local/mysql/bin"
|
|
1382
|
+
],
|
|
1383
|
+
sqlserver: [
|
|
1384
|
+
"/opt/homebrew/opt/mssql-tools18/bin",
|
|
1385
|
+
"/usr/local/opt/mssql-tools18/bin",
|
|
1386
|
+
"/opt/mssql-tools18/bin",
|
|
1387
|
+
"/opt/mssql-tools/bin"
|
|
1388
|
+
]
|
|
984
1389
|
}[type],
|
|
985
1390
|
"/usr/local/bin",
|
|
986
1391
|
"/opt/local/bin",
|
|
@@ -1011,7 +1416,9 @@ function windowsFixedDirectories(type, system, paths) {
|
|
|
1011
1416
|
sqlite: [paths.join("C:\\", "sqlite"), paths.join(programFiles, "SQLite")],
|
|
1012
1417
|
oracle: [],
|
|
1013
1418
|
hive: [],
|
|
1014
|
-
impala: []
|
|
1419
|
+
impala: [],
|
|
1420
|
+
doris: [],
|
|
1421
|
+
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
1422
|
};
|
|
1016
1423
|
return [
|
|
1017
1424
|
...localAppData === void 0 ? [] : [paths.join(localAppData, "Microsoft", "WinGet", "Links")],
|
|
@@ -1029,6 +1436,8 @@ function formulaPattern(type) {
|
|
|
1029
1436
|
case "oracle": return /^(?:oracle|instantclient)(?:@.+)?$/i;
|
|
1030
1437
|
case "hive": return /^hive(?:@.+)?$/i;
|
|
1031
1438
|
case "impala": return /^impala(?:@.+)?$/i;
|
|
1439
|
+
case "doris": return /^(?:mysql|mysql-client)(?:@.+)?$/i;
|
|
1440
|
+
case "sqlserver": return /^(?:mssql-tools|mssql-tools18)(?:@.+)?$/i;
|
|
1032
1441
|
}
|
|
1033
1442
|
}
|
|
1034
1443
|
function dynamicDirectories(type, system, paths) {
|
|
@@ -1071,7 +1480,7 @@ function dynamicDirectories(type, system, paths) {
|
|
|
1071
1480
|
});
|
|
1072
1481
|
} else if (system.platform === "win32") {
|
|
1073
1482
|
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({
|
|
1483
|
+
for (const root of roots) if (type === "mysql" || type === "doris") result.push({
|
|
1075
1484
|
root: paths.join(root, "MySQL"),
|
|
1076
1485
|
accepts: () => true,
|
|
1077
1486
|
suffix: ["bin"]
|
|
@@ -1090,6 +1499,11 @@ function dynamicDirectories(type, system, paths) {
|
|
|
1090
1499
|
accepts: () => true,
|
|
1091
1500
|
suffix: ["bin"]
|
|
1092
1501
|
});
|
|
1502
|
+
else if (type === "sqlserver") result.push({
|
|
1503
|
+
root: paths.join(root, "Microsoft SQL Server", "Client SDK", "ODBC"),
|
|
1504
|
+
accepts: () => true,
|
|
1505
|
+
suffix: ["Tools", "Binn"]
|
|
1506
|
+
});
|
|
1093
1507
|
}
|
|
1094
1508
|
return result;
|
|
1095
1509
|
}
|
|
@@ -1192,10 +1606,85 @@ function readCaptured(reader) {
|
|
|
1192
1606
|
truncated: read.lossy
|
|
1193
1607
|
};
|
|
1194
1608
|
}
|
|
1609
|
+
/** ClickHouse endpoint construction never embeds username or password. */
|
|
1610
|
+
function clickHouseConnectionUrl(connection) {
|
|
1611
|
+
const secure = connection.secure === true;
|
|
1612
|
+
const url = new URL(`${secure ? "https" : "http"}://127.0.0.1`);
|
|
1613
|
+
url.hostname = connection.host ?? "127.0.0.1";
|
|
1614
|
+
url.port = String(connection.port ?? defaultDatabasePort("clickhouse", secure));
|
|
1615
|
+
return url.toString();
|
|
1616
|
+
}
|
|
1617
|
+
async function collectClickHouseStream(stream, maxBytes, signal) {
|
|
1618
|
+
const chunks = [];
|
|
1619
|
+
let size = 0;
|
|
1620
|
+
let truncated = false;
|
|
1621
|
+
for await (const chunk of stream) {
|
|
1622
|
+
signal.throwIfAborted();
|
|
1623
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1624
|
+
const remaining = maxBytes - size;
|
|
1625
|
+
if (remaining <= 0) {
|
|
1626
|
+
truncated = true;
|
|
1627
|
+
stream.destroy?.();
|
|
1628
|
+
break;
|
|
1629
|
+
}
|
|
1630
|
+
if (buffer.byteLength > remaining) {
|
|
1631
|
+
chunks.push(buffer.subarray(0, remaining));
|
|
1632
|
+
size += remaining;
|
|
1633
|
+
truncated = true;
|
|
1634
|
+
stream.destroy?.();
|
|
1635
|
+
break;
|
|
1636
|
+
}
|
|
1637
|
+
chunks.push(buffer);
|
|
1638
|
+
size += buffer.byteLength;
|
|
1639
|
+
}
|
|
1640
|
+
return {
|
|
1641
|
+
text: Buffer.concat(chunks, size).toString("utf8"),
|
|
1642
|
+
truncated
|
|
1643
|
+
};
|
|
1644
|
+
}
|
|
1645
|
+
async function runClickHouseQuery(connection, sql, options, signal) {
|
|
1646
|
+
const client = createClient({
|
|
1647
|
+
url: clickHouseConnectionUrl(connection),
|
|
1648
|
+
username: connection.user ?? defaultDatabaseUser("clickhouse"),
|
|
1649
|
+
password: connection.password ?? "",
|
|
1650
|
+
database: connection.database,
|
|
1651
|
+
request_timeout: options.timeoutMs
|
|
1652
|
+
});
|
|
1653
|
+
try {
|
|
1654
|
+
if (classifyStatement(sql, "clickhouse") !== "read") {
|
|
1655
|
+
await client.command({
|
|
1656
|
+
query: sql,
|
|
1657
|
+
abort_signal: signal,
|
|
1658
|
+
clickhouse_settings: { wait_end_of_query: 1 }
|
|
1659
|
+
});
|
|
1660
|
+
return {
|
|
1661
|
+
exitCode: 0,
|
|
1662
|
+
stdout: "",
|
|
1663
|
+
stderr: "",
|
|
1664
|
+
truncated: false
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
const format = options.mode === "structured" ? "JSONCompactEachRowWithNamesAndTypes" : options.mode === "introspect" ? "TabSeparated" : "TabSeparatedWithNames";
|
|
1668
|
+
const { stream } = await client.exec({
|
|
1669
|
+
query: sql,
|
|
1670
|
+
abort_signal: signal,
|
|
1671
|
+
clickhouse_settings: { default_format: format }
|
|
1672
|
+
});
|
|
1673
|
+
const stdout = await collectClickHouseStream(stream, options.maxResultChars, signal);
|
|
1674
|
+
return {
|
|
1675
|
+
exitCode: 0,
|
|
1676
|
+
stdout: stdout.text,
|
|
1677
|
+
stderr: "",
|
|
1678
|
+
truncated: stdout.truncated
|
|
1679
|
+
};
|
|
1680
|
+
} finally {
|
|
1681
|
+
await client.close();
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1195
1684
|
/**
|
|
1196
|
-
* Run one SQL text through the type's
|
|
1197
|
-
* child
|
|
1198
|
-
*
|
|
1685
|
+
* Run one SQL text through the type's shared adapter. CLI SQL is written to
|
|
1686
|
+
* child stdin (`{ data }` batch disposition), while ClickHouse SQL is an HTTP
|
|
1687
|
+
* request body; neither path puts SQL or credentials in argv.
|
|
1199
1688
|
*
|
|
1200
1689
|
* Failure classification:
|
|
1201
1690
|
* - the caller's external signal (e.g. the tool exec signal) aborts → the
|
|
@@ -1213,7 +1702,6 @@ function readCaptured(reader) {
|
|
|
1213
1702
|
* @returns the captured outcome.
|
|
1214
1703
|
*/
|
|
1215
1704
|
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
1705
|
const controller = new AbortController();
|
|
1218
1706
|
const timer = setTimeout(() => controller.abort(/* @__PURE__ */ new Error(`查询超过 ${options.timeoutMs}ms 未完成,已终止客户端进程`)), options.timeoutMs);
|
|
1219
1707
|
const onExternalAbort = () => {
|
|
@@ -1222,6 +1710,12 @@ async function runClientQuery(ctx, connection, sql, options, externalSignal, int
|
|
|
1222
1710
|
if (externalSignal.aborted) controller.abort(externalSignal.reason);
|
|
1223
1711
|
else externalSignal.addEventListener("abort", onExternalAbort, { once: true });
|
|
1224
1712
|
try {
|
|
1713
|
+
if (connection.type === "clickhouse") return await runClickHouseQuery(connection, sql, {
|
|
1714
|
+
...options,
|
|
1715
|
+
mode: options.mode ?? (introspect ? "introspect" : "query")
|
|
1716
|
+
}, controller.signal);
|
|
1717
|
+
if (connection.type === "sqlserver") assertSqlServerSafeInput(sql);
|
|
1718
|
+
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
1719
|
const resolution = await resolveClientExecutable({
|
|
1226
1720
|
type: connection.type,
|
|
1227
1721
|
command: template.command,
|
|
@@ -1254,7 +1748,7 @@ async function runClientQuery(ctx, connection, sql, options, externalSignal, int
|
|
|
1254
1748
|
const stderr = readCaptured(handle.collected.stderr);
|
|
1255
1749
|
return {
|
|
1256
1750
|
exitCode: outcome.exitCode,
|
|
1257
|
-
stdout: stdout.text,
|
|
1751
|
+
stdout: connection.type === "sqlserver" ? stripSqlServerRowCountFooter(stdout.text) : stdout.text,
|
|
1258
1752
|
stderr: stderr.text,
|
|
1259
1753
|
truncated: stdout.truncated || stderr.truncated
|
|
1260
1754
|
};
|
|
@@ -1263,6 +1757,231 @@ async function runClientQuery(ctx, connection, sql, options, externalSignal, int
|
|
|
1263
1757
|
externalSignal.removeEventListener("abort", onExternalAbort);
|
|
1264
1758
|
}
|
|
1265
1759
|
}
|
|
1760
|
+
//#endregion
|
|
1761
|
+
//#region src/structured.ts
|
|
1762
|
+
function normalizeNewlines(text) {
|
|
1763
|
+
return text.replace(/\r\n?/g, "\n");
|
|
1764
|
+
}
|
|
1765
|
+
function splitLine(line, delimiter) {
|
|
1766
|
+
return line.split(delimiter);
|
|
1767
|
+
}
|
|
1768
|
+
/** Make column names valid unique JSON object keys. */
|
|
1769
|
+
function uniqueColumns(columns) {
|
|
1770
|
+
const used = /* @__PURE__ */ new Set();
|
|
1771
|
+
return columns.map((raw, index) => {
|
|
1772
|
+
let name = raw.trim();
|
|
1773
|
+
if (name.length === 0) name = `column_${index + 1}`;
|
|
1774
|
+
if (used.has(name)) {
|
|
1775
|
+
let suffix = 2;
|
|
1776
|
+
while (used.has(`${name}_${suffix}`)) suffix += 1;
|
|
1777
|
+
name = `${name}_${suffix}`;
|
|
1778
|
+
}
|
|
1779
|
+
used.add(name);
|
|
1780
|
+
return name;
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
function rowObject(columns, fields) {
|
|
1784
|
+
const row = {};
|
|
1785
|
+
for (let index = 0; index < columns.length; index += 1) row[columns[index]] = fields[index] ?? null;
|
|
1786
|
+
return row;
|
|
1787
|
+
}
|
|
1788
|
+
function emptyOutput() {
|
|
1789
|
+
return {
|
|
1790
|
+
columns: [],
|
|
1791
|
+
rows: [],
|
|
1792
|
+
rowLimitExceeded: false
|
|
1793
|
+
};
|
|
1794
|
+
}
|
|
1795
|
+
function skipLeadingBlank(lines) {
|
|
1796
|
+
let index = 0;
|
|
1797
|
+
while (index < lines.length && lines[index].trim().length === 0) index += 1;
|
|
1798
|
+
return index;
|
|
1799
|
+
}
|
|
1800
|
+
/** PostgreSQL `-A` appends a `(N rows)` / `(N row)` footer after SELECT output. */
|
|
1801
|
+
function isPostgresFooter(line) {
|
|
1802
|
+
return /^\(\d+ rows?\)$/.test(line.trim());
|
|
1803
|
+
}
|
|
1804
|
+
function parseDelimited(stdout, delimiter, maxRows, skipFooter = false) {
|
|
1805
|
+
const lines = normalizeNewlines(stdout).split("\n");
|
|
1806
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1807
|
+
const headerIndex = skipLeadingBlank(lines);
|
|
1808
|
+
if (headerIndex >= lines.length) return emptyOutput();
|
|
1809
|
+
const columns = uniqueColumns(splitLine(lines[headerIndex], delimiter));
|
|
1810
|
+
const rows = [];
|
|
1811
|
+
let rowLimitExceeded = false;
|
|
1812
|
+
for (let index = headerIndex + 1; index < lines.length; index += 1) {
|
|
1813
|
+
const line = lines[index];
|
|
1814
|
+
if (skipFooter && isPostgresFooter(line)) continue;
|
|
1815
|
+
if (rows.length >= maxRows) {
|
|
1816
|
+
rowLimitExceeded = true;
|
|
1817
|
+
break;
|
|
1818
|
+
}
|
|
1819
|
+
rows.push(rowObject(columns, splitLine(line, delimiter)));
|
|
1820
|
+
}
|
|
1821
|
+
return {
|
|
1822
|
+
columns,
|
|
1823
|
+
rows,
|
|
1824
|
+
rowLimitExceeded
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
/** Minimal RFC-4180-style parser for sqlite3 `-csv` output. */
|
|
1828
|
+
function parseCsv(text) {
|
|
1829
|
+
const records = [];
|
|
1830
|
+
let record = [];
|
|
1831
|
+
let field = "";
|
|
1832
|
+
let quoted = false;
|
|
1833
|
+
let index = 0;
|
|
1834
|
+
const pushField = () => {
|
|
1835
|
+
record.push(field);
|
|
1836
|
+
field = "";
|
|
1837
|
+
};
|
|
1838
|
+
const pushRecord = () => {
|
|
1839
|
+
pushField();
|
|
1840
|
+
records.push(record);
|
|
1841
|
+
record = [];
|
|
1842
|
+
};
|
|
1843
|
+
while (index < text.length) {
|
|
1844
|
+
const char = text[index];
|
|
1845
|
+
if (quoted) {
|
|
1846
|
+
if (char === "\"") {
|
|
1847
|
+
if (text[index + 1] === "\"") {
|
|
1848
|
+
field += "\"";
|
|
1849
|
+
index += 2;
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
quoted = false;
|
|
1853
|
+
index += 1;
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
field += char;
|
|
1857
|
+
index += 1;
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
if (char === "\"" && field.length === 0) {
|
|
1861
|
+
quoted = true;
|
|
1862
|
+
index += 1;
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
if (char === ",") {
|
|
1866
|
+
pushField();
|
|
1867
|
+
index += 1;
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
if (char === "\n") {
|
|
1871
|
+
pushRecord();
|
|
1872
|
+
index += 1;
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
if (char === "\r") {
|
|
1876
|
+
if (text[index + 1] === "\n") index += 1;
|
|
1877
|
+
pushRecord();
|
|
1878
|
+
index += 1;
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
field += char;
|
|
1882
|
+
index += 1;
|
|
1883
|
+
}
|
|
1884
|
+
if (field.length > 0 || record.length > 0) pushRecord();
|
|
1885
|
+
return records;
|
|
1886
|
+
}
|
|
1887
|
+
function parseCsvOutput(stdout, maxRows) {
|
|
1888
|
+
const records = parseCsv(normalizeNewlines(stdout)).filter((record) => !(record.length === 1 && record[0] === ""));
|
|
1889
|
+
if (records.length === 0) return emptyOutput();
|
|
1890
|
+
const columns = uniqueColumns(records[0]);
|
|
1891
|
+
const rows = [];
|
|
1892
|
+
let rowLimitExceeded = false;
|
|
1893
|
+
for (let index = 1; index < records.length; index += 1) {
|
|
1894
|
+
if (rows.length >= maxRows) {
|
|
1895
|
+
rowLimitExceeded = true;
|
|
1896
|
+
break;
|
|
1897
|
+
}
|
|
1898
|
+
rows.push(rowObject(columns, records[index]));
|
|
1899
|
+
}
|
|
1900
|
+
return {
|
|
1901
|
+
columns,
|
|
1902
|
+
rows,
|
|
1903
|
+
rowLimitExceeded
|
|
1904
|
+
};
|
|
1905
|
+
}
|
|
1906
|
+
function parseClickHouseOutput(stdout, maxRows) {
|
|
1907
|
+
const lines = normalizeNewlines(stdout).split("\n").filter((line) => line.trim() !== "");
|
|
1908
|
+
if (lines.length === 0) return emptyOutput();
|
|
1909
|
+
const parsed = lines.map((line) => JSON.parse(line));
|
|
1910
|
+
if (!Array.isArray(parsed[0])) throw new Error("ClickHouse结构化输出缺少列名行");
|
|
1911
|
+
const columns = uniqueColumns(parsed[0].map((value) => String(value)));
|
|
1912
|
+
const firstDataIndex = parsed.length > 1 && Array.isArray(parsed[1]) ? 2 : 1;
|
|
1913
|
+
const rows = [];
|
|
1914
|
+
let rowLimitExceeded = false;
|
|
1915
|
+
for (let index = firstDataIndex; index < parsed.length; index += 1) {
|
|
1916
|
+
if (rows.length >= maxRows) {
|
|
1917
|
+
rowLimitExceeded = true;
|
|
1918
|
+
break;
|
|
1919
|
+
}
|
|
1920
|
+
const record = parsed[index];
|
|
1921
|
+
if (!Array.isArray(record)) throw new Error("ClickHouse结构化输出包含非数组数据行");
|
|
1922
|
+
rows.push(rowObject(columns, record.map((value) => {
|
|
1923
|
+
if (value === null || value === void 0) return null;
|
|
1924
|
+
return typeof value === "object" ? JSON.stringify(value) : String(value);
|
|
1925
|
+
})));
|
|
1926
|
+
}
|
|
1927
|
+
return {
|
|
1928
|
+
columns,
|
|
1929
|
+
rows,
|
|
1930
|
+
rowLimitExceeded
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
function parseSqlServerOutput(stdout, maxRows) {
|
|
1934
|
+
const lines = normalizeNewlines(stripSqlServerRowCountFooter(stdout)).split("\n");
|
|
1935
|
+
if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
|
|
1936
|
+
const headerIndex = skipLeadingBlank(lines);
|
|
1937
|
+
if (headerIndex >= lines.length) return emptyOutput();
|
|
1938
|
+
const columns = uniqueColumns(lines[headerIndex].split(""));
|
|
1939
|
+
let dataIndex = headerIndex + 1;
|
|
1940
|
+
const divider = lines[dataIndex]?.split("");
|
|
1941
|
+
if (divider !== void 0 && divider.length === columns.length && divider.every((field) => /^-+$/.test(field.trim()))) dataIndex += 1;
|
|
1942
|
+
const rows = [];
|
|
1943
|
+
let rowLimitExceeded = false;
|
|
1944
|
+
for (let index = dataIndex; index < lines.length; index += 1) {
|
|
1945
|
+
if (lines[index].trim() === "") continue;
|
|
1946
|
+
if (rows.length >= maxRows) {
|
|
1947
|
+
rowLimitExceeded = true;
|
|
1948
|
+
break;
|
|
1949
|
+
}
|
|
1950
|
+
const fields = lines[index].split("");
|
|
1951
|
+
const row = {};
|
|
1952
|
+
for (let column = 0; column < columns.length; column += 1) {
|
|
1953
|
+
const value = fields[column];
|
|
1954
|
+
row[columns[column]] = value === void 0 || value === "NULL" ? null : value;
|
|
1955
|
+
}
|
|
1956
|
+
rows.push(row);
|
|
1957
|
+
}
|
|
1958
|
+
return {
|
|
1959
|
+
columns,
|
|
1960
|
+
rows,
|
|
1961
|
+
rowLimitExceeded
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Parse one database type's structured-query stdout. The matching template is
|
|
1966
|
+
* `buildStructuredQueryTemplate`: mysql tab-separated with a header, postgres
|
|
1967
|
+
* pipe-separated with a header and row-count footer, sqlite CSV with a header,
|
|
1968
|
+
* oracle pipe-separated with heading on, hive/impala tsv with a header.
|
|
1969
|
+
*/
|
|
1970
|
+
function parseStructuredQueryOutput(type, stdout, maxRows) {
|
|
1971
|
+
switch (type) {
|
|
1972
|
+
case "mysql": return parseDelimited(stdout, " ", maxRows);
|
|
1973
|
+
case "doris": return parseDelimited(stdout, " ", maxRows);
|
|
1974
|
+
case "clickhouse": return parseClickHouseOutput(stdout, maxRows);
|
|
1975
|
+
case "postgres": return parseDelimited(stdout, "|", maxRows, true);
|
|
1976
|
+
case "sqlite": return parseCsvOutput(stdout, maxRows);
|
|
1977
|
+
case "oracle": return parseDelimited(stdout, "|", maxRows);
|
|
1978
|
+
case "hive":
|
|
1979
|
+
case "impala": return parseDelimited(stdout, " ", maxRows);
|
|
1980
|
+
case "sqlserver": return parseSqlServerOutput(stdout, maxRows);
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
const MYSQL_SCHEMA_PROBE_CONCURRENCY = 4;
|
|
1984
|
+
const MYSQL_DATABASE_ACCESS_DENIED = /\bERROR\s+1044\s+\(42000\)/i;
|
|
1266
1985
|
/** Build a password-stripped copy of one connection. */
|
|
1267
1986
|
function summarize(connection) {
|
|
1268
1987
|
const summary = {
|
|
@@ -1274,6 +1993,7 @@ function summarize(connection) {
|
|
|
1274
1993
|
if (connection.user !== void 0) summary.user = connection.user;
|
|
1275
1994
|
if (connection.passwordRef !== void 0) summary.passwordRef = connection.passwordRef;
|
|
1276
1995
|
if (connection.readonly !== void 0) summary.readonly = connection.readonly;
|
|
1996
|
+
if (connection.secure !== void 0) summary.secure = connection.secure;
|
|
1277
1997
|
if (connection.profileId !== void 0) summary.profileId = connection.profileId;
|
|
1278
1998
|
if (connection.name !== void 0) summary.name = connection.name;
|
|
1279
1999
|
if (connection.tables !== void 0) summary.tables = [...connection.tables];
|
|
@@ -1303,19 +2023,22 @@ function normalizeConnectionInput(input, cwd = process.cwd()) {
|
|
|
1303
2023
|
if (input.port !== void 0 && (!Number.isInteger(input.port) || input.port < 1 || input.port > 65535)) throw new Error("port 必须是 1-65535 的整数");
|
|
1304
2024
|
if (input.profileId !== void 0 && input.profileId.trim().length === 0) throw new Error("profileId 不能为空");
|
|
1305
2025
|
if (input.name !== void 0 && input.name.trim().length === 0) throw new Error("name 不能为空");
|
|
2026
|
+
if (input.secure !== void 0 && typeof input.secure !== "boolean") throw new Error("secure 必须是布尔值");
|
|
1306
2027
|
const connection = {
|
|
1307
2028
|
type: input.type,
|
|
1308
2029
|
database: input.type === "sqlite" ? resolve(cwd, input.database) : input.database,
|
|
1309
2030
|
credentialMode: input.type === "sqlite" ? "none" : input.passwordRef !== void 0 ? "reference" : input.password !== void 0 && input.password.length > 0 ? "password" : "none"
|
|
1310
2031
|
};
|
|
1311
2032
|
if (input.type !== "sqlite") {
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
2033
|
+
connection.host = input.host !== void 0 && input.host.length > 0 ? input.host : "127.0.0.1";
|
|
2034
|
+
connection.port = input.port ?? defaultDatabasePort(input.type, input.type === "clickhouse" && input.secure === true);
|
|
2035
|
+
const user = input.user !== void 0 && input.user.length > 0 ? input.user : defaultDatabaseUser(input.type);
|
|
2036
|
+
if (user !== "") connection.user = user;
|
|
1315
2037
|
if (input.password !== void 0 && input.password.length > 0) connection.password = input.password;
|
|
1316
2038
|
if (input.passwordRef !== void 0) connection.passwordRef = input.passwordRef;
|
|
1317
2039
|
}
|
|
1318
2040
|
if (input.readonly !== void 0) connection.readonly = input.readonly;
|
|
2041
|
+
if (input.type === "clickhouse" && input.secure !== void 0) connection.secure = input.secure;
|
|
1319
2042
|
if (input.profileId !== void 0) connection.profileId = input.profileId;
|
|
1320
2043
|
if (input.name !== void 0) connection.name = input.name;
|
|
1321
2044
|
return connection;
|
|
@@ -1333,6 +2056,7 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1333
2056
|
};
|
|
1334
2057
|
const runtime = /* @__PURE__ */ new Map();
|
|
1335
2058
|
const formDrafts = /* @__PURE__ */ new Map();
|
|
2059
|
+
let latestFormInitial;
|
|
1336
2060
|
const profileConnection = (sessionId) => {
|
|
1337
2061
|
if (persistence === void 0) return void 0;
|
|
1338
2062
|
const binding = persistence.getBinding(sessionId);
|
|
@@ -1365,15 +2089,15 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1365
2089
|
tables: copyTables(connection.tables)
|
|
1366
2090
|
};
|
|
1367
2091
|
};
|
|
1368
|
-
const queryOptions = (mode, connect = false) => ({
|
|
2092
|
+
const queryOptions = (mode, connect = false, maxResultChars = resolvedOptions.maxResultChars, catalog = false) => ({
|
|
1369
2093
|
clients: resolvedOptions.clients,
|
|
1370
|
-
timeoutMs: connect ? resolvedOptions.connectTimeoutMs : resolvedOptions.queryTimeoutMs,
|
|
1371
|
-
maxResultChars
|
|
2094
|
+
timeoutMs: connect ? resolvedOptions.connectTimeoutMs : catalog ? resolvedOptions.catalogQueryTimeoutMs ?? resolvedOptions.queryTimeoutMs : resolvedOptions.queryTimeoutMs,
|
|
2095
|
+
maxResultChars,
|
|
1372
2096
|
...mode !== void 0 ? { mode } : {}
|
|
1373
2097
|
});
|
|
1374
|
-
const run = async (connection, sql, signal, introspection = false, connect = false) => {
|
|
2098
|
+
const run = async (connection, sql, signal, introspection = false, connect = false, mode, maxResultChars, catalog = false) => {
|
|
1375
2099
|
try {
|
|
1376
|
-
return redactQueryResult(await runClientQuery(requireContext(), connection, sql, queryOptions(
|
|
2100
|
+
return redactQueryResult(await runClientQuery(requireContext(), connection, sql, queryOptions(mode, connect, maxResultChars, catalog), signal, introspection), connection);
|
|
1377
2101
|
} catch (error) {
|
|
1378
2102
|
const message = redactSecretText(error instanceof Error ? error.message : String(error), [connection.password]);
|
|
1379
2103
|
throw new Error(message, error instanceof Error ? { cause: error } : void 0);
|
|
@@ -1387,17 +2111,48 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1387
2111
|
}
|
|
1388
2112
|
return parseTableListing(connection.type, result.stdout).slice(0, resolvedOptions.introspectMaxTables);
|
|
1389
2113
|
};
|
|
1390
|
-
const
|
|
2114
|
+
const canAccessMySqlSchema = async (connection, schema, signal) => {
|
|
2115
|
+
if (schema === connection.database) return true;
|
|
2116
|
+
const result = await run({
|
|
2117
|
+
...connection,
|
|
2118
|
+
database: schema
|
|
2119
|
+
}, "SHOW TABLES;", signal, true);
|
|
2120
|
+
if (result.exitCode === 0) return true;
|
|
2121
|
+
const detail = result.stderr.trim() !== "" ? result.stderr.trim() : result.stdout.trim();
|
|
2122
|
+
if (MYSQL_DATABASE_ACCESS_DENIED.test(detail)) return false;
|
|
2123
|
+
throw new Error(`元数据查询失败(exit ${result.exitCode}):${detail}`);
|
|
2124
|
+
};
|
|
2125
|
+
const listAccessibleMySqlSchemas = async (connection, schemas, signal) => {
|
|
2126
|
+
const visible = [];
|
|
2127
|
+
for (let offset = 0; offset < schemas.length && visible.length < resolvedOptions.introspectMaxTables; offset += MYSQL_SCHEMA_PROBE_CONCURRENCY) {
|
|
2128
|
+
signal.throwIfAborted();
|
|
2129
|
+
const batch = schemas.slice(offset, offset + MYSQL_SCHEMA_PROBE_CONCURRENCY);
|
|
2130
|
+
const accessible = await Promise.all(batch.map((schema) => canAccessMySqlSchema(connection, schema, signal)));
|
|
2131
|
+
for (let index = 0; index < batch.length; index += 1) {
|
|
2132
|
+
if (accessible[index]) visible.push(batch[index]);
|
|
2133
|
+
if (visible.length === resolvedOptions.introspectMaxTables) return visible;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
return visible;
|
|
2137
|
+
};
|
|
2138
|
+
const persistAtomically = async (sessionId, profileId, profile, draft) => {
|
|
1391
2139
|
if (persistence === void 0) return;
|
|
1392
2140
|
const previousProfile = persistence.getProfile(profileId);
|
|
1393
2141
|
const previousBinding = persistence.getBinding(sessionId);
|
|
2142
|
+
const previousDraft = persistence.getDraft?.(sessionId);
|
|
1394
2143
|
await persistence.putProfile(profileId, profile);
|
|
1395
2144
|
try {
|
|
1396
2145
|
await persistence.putBinding(sessionId, {
|
|
1397
2146
|
profileId,
|
|
1398
2147
|
updatedAt: profile.updatedAt
|
|
1399
2148
|
});
|
|
2149
|
+
await persistence.putDraft?.(sessionId, {
|
|
2150
|
+
...draft,
|
|
2151
|
+
updatedAt: profile.updatedAt
|
|
2152
|
+
});
|
|
1400
2153
|
} catch (error) {
|
|
2154
|
+
if (previousDraft === void 0) await persistence.deleteDraft?.(sessionId);
|
|
2155
|
+
else await persistence.putDraft?.(sessionId, previousDraft);
|
|
1401
2156
|
if (previousProfile === void 0) await persistence.deleteProfile(profileId);
|
|
1402
2157
|
else await persistence.putProfile(profileId, previousProfile);
|
|
1403
2158
|
if (previousBinding === void 0) await persistence.deleteBinding(sessionId);
|
|
@@ -1405,6 +2160,50 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1405
2160
|
throw error;
|
|
1406
2161
|
}
|
|
1407
2162
|
};
|
|
2163
|
+
const matchingProfiles = (connection) => (persistence?.listProfiles?.() ?? []).filter((entry) => profileMatchesConnection(entry.profile, connection, resolvedOptions.cwd));
|
|
2164
|
+
const preferredMatches = (matches) => {
|
|
2165
|
+
const preferred = new Set(resolvedOptions.preferredProfileIds?.() ?? []);
|
|
2166
|
+
return matches.filter((entry) => preferred.has(entry.profileId));
|
|
2167
|
+
};
|
|
2168
|
+
const reusableProfileId = (sessionId, connection) => {
|
|
2169
|
+
if (connection.profileId !== void 0) return connection.profileId;
|
|
2170
|
+
const fallback = `session:${sessionId}`;
|
|
2171
|
+
const matches = matchingProfiles(connection);
|
|
2172
|
+
const binding = persistence?.getBinding(sessionId);
|
|
2173
|
+
const boundMatch = binding === void 0 ? void 0 : matches.find((entry) => entry.profileId === binding.profileId);
|
|
2174
|
+
const preferred = preferredMatches(matches);
|
|
2175
|
+
const stableMatches = matches.filter((entry) => !entry.profileId.startsWith("session:"));
|
|
2176
|
+
if (boundMatch !== void 0 && preferred.some((entry) => entry.profileId === boundMatch.profileId)) return boundMatch.profileId;
|
|
2177
|
+
if (preferred.length === 1) return preferred[0].profileId;
|
|
2178
|
+
if (preferred.length > 1) return fallback;
|
|
2179
|
+
if (boundMatch !== void 0 && !boundMatch.profileId.startsWith("session:")) return boundMatch.profileId;
|
|
2180
|
+
if (stableMatches.length === 1) return stableMatches[0].profileId;
|
|
2181
|
+
if (stableMatches.length > 1) return fallback;
|
|
2182
|
+
if (boundMatch !== void 0) return boundMatch.profileId;
|
|
2183
|
+
return matches.length === 1 ? matches[0].profileId : fallback;
|
|
2184
|
+
};
|
|
2185
|
+
const reconcileStableProfile = async (sessionId, connection) => {
|
|
2186
|
+
if (persistence === void 0 || connection.profileId === void 0) return connection;
|
|
2187
|
+
const matches = matchingProfiles(connection);
|
|
2188
|
+
const preferred = preferredMatches(matches);
|
|
2189
|
+
if (preferred.some((entry) => entry.profileId === connection.profileId)) return connection;
|
|
2190
|
+
const stableMatches = matches.filter((entry) => !entry.profileId.startsWith("session:"));
|
|
2191
|
+
const target = preferred.length === 1 ? preferred[0] : connection.profileId.startsWith("session:") ? stableMatches.length === 1 ? stableMatches[0] : void 0 : void 0;
|
|
2192
|
+
if (target === void 0) return connection;
|
|
2193
|
+
const profileId = target.profileId;
|
|
2194
|
+
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
2195
|
+
await persistence.putBinding(sessionId, {
|
|
2196
|
+
profileId,
|
|
2197
|
+
updatedAt
|
|
2198
|
+
});
|
|
2199
|
+
const reconciled = {
|
|
2200
|
+
...connection,
|
|
2201
|
+
profileId,
|
|
2202
|
+
tables: copyTables(connection.tables)
|
|
2203
|
+
};
|
|
2204
|
+
runtime.set(sessionId, reconciled);
|
|
2205
|
+
return reconciled;
|
|
2206
|
+
};
|
|
1408
2207
|
const credentialSummary = async (connection) => {
|
|
1409
2208
|
const mode = credentialModeOf(connection);
|
|
1410
2209
|
if (connection.type === "sqlite" || mode === "none") return void 0;
|
|
@@ -1457,7 +2256,15 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1457
2256
|
},
|
|
1458
2257
|
getFormDraft(sessionId) {
|
|
1459
2258
|
const draft = persistence?.getDraft?.(sessionId) ?? formDrafts.get(sessionId);
|
|
1460
|
-
|
|
2259
|
+
const exactProfile = profileConnection(sessionId);
|
|
2260
|
+
if (draft !== void 0) return {
|
|
2261
|
+
...copyFormDraft(draft),
|
|
2262
|
+
...exactProfile?.passwordRef !== void 0 ? { passwordRef: exactProfile.passwordRef } : {}
|
|
2263
|
+
};
|
|
2264
|
+
if (exactProfile !== void 0) return formInitialFromConnection(exactProfile);
|
|
2265
|
+
const latestProfile = persistence?.getLatestProfile?.();
|
|
2266
|
+
if (latestProfile !== void 0) return formInitialFromConnection(connectionFromProfile(latestProfile.profileId, latestProfile.profile));
|
|
2267
|
+
return latestFormInitial === void 0 ? void 0 : copyFormInitial(latestFormInitial);
|
|
1461
2268
|
},
|
|
1462
2269
|
async saveFormDraft(sessionId, draft) {
|
|
1463
2270
|
if (sessionId.length === 0) throw new Error("sessionId 必须是非空字符串");
|
|
@@ -1471,16 +2278,19 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1471
2278
|
async status(sessionId) {
|
|
1472
2279
|
const connection = rawConnection(sessionId);
|
|
1473
2280
|
if (connection === void 0) return void 0;
|
|
1474
|
-
return statusSummary(connection);
|
|
2281
|
+
return statusSummary(await reconcileStableProfile(sessionId, connection));
|
|
1475
2282
|
},
|
|
1476
2283
|
async connect(sessionId, input, signal) {
|
|
1477
2284
|
if (sessionId.length === 0) throw new Error("sessionId 必须是非空字符串");
|
|
1478
2285
|
const normalized = normalizeConnectionInput(input, resolvedOptions.cwd);
|
|
1479
2286
|
const execution = await resolveCredential(normalized);
|
|
1480
2287
|
const tables = await verify(execution, signal, true);
|
|
1481
|
-
const profileId = normalized
|
|
2288
|
+
const profileId = reusableProfileId(sessionId, normalized);
|
|
1482
2289
|
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1483
|
-
|
|
2290
|
+
const draft = formDraftFromConnection(normalized);
|
|
2291
|
+
await persistAtomically(sessionId, profileId, profileFromConnection(normalized, updatedAt), draft);
|
|
2292
|
+
if (persistence === void 0) formDrafts.set(sessionId, draft);
|
|
2293
|
+
latestFormInitial = formInitialFromConnection(normalized);
|
|
1484
2294
|
const published = {
|
|
1485
2295
|
...normalized,
|
|
1486
2296
|
profileId,
|
|
@@ -1514,10 +2324,26 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1514
2324
|
if (connection === void 0) throw new Error("请先在 Web「数据库」标签页连接数据库,或在 TUI 运行 /database connect(未找到当前会话的连接)");
|
|
1515
2325
|
return resolveCredential(connection);
|
|
1516
2326
|
},
|
|
2327
|
+
async queryMetadata(sessionId, sql, signal) {
|
|
2328
|
+
if (sql.trim().length === 0) throw new Error("Catalog metadata SQL must not be empty");
|
|
2329
|
+
const maxQueryChars = resolvedOptions.maxQueryChars ?? 65536;
|
|
2330
|
+
if (sql.length > maxQueryChars) throw new Error(`Catalog metadata SQL exceeds ${maxQueryChars} characters`);
|
|
2331
|
+
assertSingleStatement(sql, "Catalog metadata query");
|
|
2332
|
+
const connection = await service.resolveForExecution(sessionId);
|
|
2333
|
+
if (classifyStatement(sql, connection.type) !== "read") throw new Error("Catalog metadata execution accepts read-only system catalog statements only");
|
|
2334
|
+
const result = await run(connection, sql, signal, true, false, void 0, resolvedOptions.catalogMaxResultChars ?? resolvedOptions.maxResultChars, true);
|
|
2335
|
+
if (result.exitCode !== 0) {
|
|
2336
|
+
const detail = result.stderr.trim() !== "" ? result.stderr.trim() : result.stdout.trim();
|
|
2337
|
+
throw new Error(`Catalog metadata query failed (exit ${result.exitCode}): ${detail}`);
|
|
2338
|
+
}
|
|
2339
|
+
return result;
|
|
2340
|
+
},
|
|
1517
2341
|
async listSchemas(sessionId, signal) {
|
|
1518
2342
|
const connection = await service.resolveForExecution(sessionId);
|
|
1519
2343
|
const stdout = await runMetadata(connection, "schemas", signal);
|
|
1520
|
-
|
|
2344
|
+
const schemas = parseListing(connection.type, stdout);
|
|
2345
|
+
if (connection.type === "mysql" || connection.type === "doris") return listAccessibleMySqlSchemas(connection, schemas, signal);
|
|
2346
|
+
return schemas.slice(0, resolvedOptions.introspectMaxTables);
|
|
1521
2347
|
},
|
|
1522
2348
|
async listTables(sessionId, schema, signal) {
|
|
1523
2349
|
const connection = await service.resolveForExecution(sessionId);
|
|
@@ -1540,6 +2366,36 @@ function createConnectionService(ctx, options, persistence) {
|
|
|
1540
2366
|
const connection = await service.resolveForExecution(sessionId);
|
|
1541
2367
|
if ((connection.readonly ?? resolvedOptions.readonly) && classifyStatement(sql, connection.type) === "write") throw new Error("当前连接为只读模式,拒绝执行非读语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/PRAGMA 等)");
|
|
1542
2368
|
return run(connection, sql, signal);
|
|
2369
|
+
},
|
|
2370
|
+
async executeInteractive(sessionId, sql, signal) {
|
|
2371
|
+
if (sql.trim().length === 0) throw new Error("sql 必须是非空字符串");
|
|
2372
|
+
const maxQueryChars = resolvedOptions.maxQueryChars ?? 65536;
|
|
2373
|
+
if (sql.length > maxQueryChars) throw new Error(`sql 超过长度上限(${maxQueryChars} 字符)`);
|
|
2374
|
+
assertSingleStatement(sql, "/query");
|
|
2375
|
+
const connection = await service.resolveForExecution(sessionId);
|
|
2376
|
+
const statementKind = classifyStatement(sql, connection.type);
|
|
2377
|
+
if ((connection.readonly ?? resolvedOptions.readonly) && statementKind === "write") throw new Error("当前连接为只读模式,拒绝执行非读语句(仅放行 SELECT/SHOW/DESCRIBE/EXPLAIN/PRAGMA 等)");
|
|
2378
|
+
if (statementKind === "write") return {
|
|
2379
|
+
kind: "message",
|
|
2380
|
+
...await run(connection, sql, signal)
|
|
2381
|
+
};
|
|
2382
|
+
const limitedSql = enforceReadRowLimit(sql, connection.type, 50001);
|
|
2383
|
+
const startedAt = Date.now();
|
|
2384
|
+
const result = await run(connection, limitedSql, signal, false, false, "structured", WORKBENCH_MAX_RESULT_CHARS);
|
|
2385
|
+
if (result.exitCode !== 0) return {
|
|
2386
|
+
kind: "message",
|
|
2387
|
+
...result
|
|
2388
|
+
};
|
|
2389
|
+
if (result.truncated) throw new Error("查询结果超过 Web 工作台大小上限,请减少返回列或缩小字段后重试");
|
|
2390
|
+
const parsed = parseStructuredQueryOutput(connection.type, result.stdout, WORKBENCH_MAX_EXPORT_ROWS);
|
|
2391
|
+
return {
|
|
2392
|
+
kind: "table",
|
|
2393
|
+
columns: parsed.columns,
|
|
2394
|
+
rows: parsed.rows,
|
|
2395
|
+
elapsedMs: Date.now() - startedAt,
|
|
2396
|
+
truncated: parsed.rowLimitExceeded,
|
|
2397
|
+
maxRows: WORKBENCH_MAX_EXPORT_ROWS
|
|
2398
|
+
};
|
|
1543
2399
|
}
|
|
1544
2400
|
};
|
|
1545
2401
|
async function runMetadata(connection, kind, signal, schema, table) {
|
|
@@ -1557,7 +2413,7 @@ function copyTables(tables) {
|
|
|
1557
2413
|
}
|
|
1558
2414
|
function normalizeFormDraft(draft) {
|
|
1559
2415
|
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("数据库表单草稿无效");
|
|
2416
|
+
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
2417
|
return copyFormDraft(draft);
|
|
1562
2418
|
}
|
|
1563
2419
|
function copyFormDraft(draft) {
|
|
@@ -1567,11 +2423,32 @@ function copyFormDraft(draft) {
|
|
|
1567
2423
|
port: draft.port,
|
|
1568
2424
|
user: draft.user,
|
|
1569
2425
|
database: draft.database,
|
|
1570
|
-
readonly: draft.readonly
|
|
2426
|
+
readonly: draft.readonly,
|
|
2427
|
+
...draft.type === "clickhouse" ? { secure: draft.secure ?? false } : {}
|
|
1571
2428
|
};
|
|
1572
2429
|
}
|
|
1573
|
-
function
|
|
1574
|
-
return
|
|
2430
|
+
function copyFormInitial(initial) {
|
|
2431
|
+
return {
|
|
2432
|
+
...copyFormDraft(initial),
|
|
2433
|
+
...initial.passwordRef !== void 0 ? { passwordRef: initial.passwordRef } : {}
|
|
2434
|
+
};
|
|
2435
|
+
}
|
|
2436
|
+
function formDraftFromConnection(connection) {
|
|
2437
|
+
return {
|
|
2438
|
+
type: connection.type,
|
|
2439
|
+
host: connection.type === "sqlite" ? "" : connection.host ?? "",
|
|
2440
|
+
port: connection.type === "sqlite" || connection.port === void 0 ? "" : String(connection.port),
|
|
2441
|
+
user: connection.type === "sqlite" ? "" : connection.user ?? "",
|
|
2442
|
+
database: connection.database,
|
|
2443
|
+
readonly: connection.readonly ?? false,
|
|
2444
|
+
...connection.type === "clickhouse" ? { secure: connection.secure ?? false } : {}
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
function formInitialFromConnection(connection) {
|
|
2448
|
+
return {
|
|
2449
|
+
...formDraftFromConnection(connection),
|
|
2450
|
+
...connection.passwordRef !== void 0 ? { passwordRef: connection.passwordRef } : {}
|
|
2451
|
+
};
|
|
1575
2452
|
}
|
|
1576
2453
|
function validatePasswordRef(value) {
|
|
1577
2454
|
try {
|
|
@@ -1594,6 +2471,7 @@ function connectionFromProfile(profileId, profile) {
|
|
|
1594
2471
|
...profile.port !== void 0 ? { port: profile.port } : {},
|
|
1595
2472
|
...profile.user !== void 0 ? { user: profile.user } : {},
|
|
1596
2473
|
...profile.readonly !== void 0 ? { readonly: profile.readonly } : {},
|
|
2474
|
+
...profile.secure !== void 0 ? { secure: profile.secure } : {},
|
|
1597
2475
|
...profile.passwordRef !== void 0 ? { passwordRef: profile.passwordRef } : {},
|
|
1598
2476
|
credentialMode: profile.credentialMode ?? (profile.type === "sqlite" ? "none" : profile.passwordRef !== void 0 ? "reference" : "password")
|
|
1599
2477
|
};
|
|
@@ -1608,10 +2486,28 @@ function profileFromConnection(connection, updatedAt) {
|
|
|
1608
2486
|
...connection.port !== void 0 ? { port: connection.port } : {},
|
|
1609
2487
|
...connection.user !== void 0 ? { user: connection.user } : {},
|
|
1610
2488
|
...connection.readonly !== void 0 ? { readonly: connection.readonly } : {},
|
|
2489
|
+
...connection.secure !== void 0 ? { secure: connection.secure } : {},
|
|
1611
2490
|
...connection.passwordRef !== void 0 ? { passwordRef: connection.passwordRef } : {},
|
|
1612
2491
|
...connection.credentialMode !== void 0 ? { credentialMode: connection.credentialMode } : {}
|
|
1613
2492
|
};
|
|
1614
2493
|
}
|
|
2494
|
+
/** Match only normalized, non-secret endpoint/principal identity fields. */
|
|
2495
|
+
function profileMatchesConnection(profile, connection, cwd = process.cwd()) {
|
|
2496
|
+
let candidate;
|
|
2497
|
+
try {
|
|
2498
|
+
candidate = normalizeConnectionInput({
|
|
2499
|
+
type: profile.type,
|
|
2500
|
+
database: profile.database,
|
|
2501
|
+
...profile.host !== void 0 ? { host: profile.host } : {},
|
|
2502
|
+
...profile.port !== void 0 ? { port: profile.port } : {},
|
|
2503
|
+
...profile.user !== void 0 ? { user: profile.user } : {},
|
|
2504
|
+
...profile.secure !== void 0 ? { secure: profile.secure } : {}
|
|
2505
|
+
}, cwd);
|
|
2506
|
+
} catch {
|
|
2507
|
+
return false;
|
|
2508
|
+
}
|
|
2509
|
+
return candidate.type === connection.type && candidate.database === connection.database && candidate.host === connection.host && candidate.port === connection.port && candidate.user === connection.user && (candidate.secure ?? false) === (connection.secure ?? false);
|
|
2510
|
+
}
|
|
1615
2511
|
/** Infer legacy records while leaving ambiguous secret-less SQL profiles conservative. */
|
|
1616
2512
|
function credentialModeOf(connection) {
|
|
1617
2513
|
if (connection.credentialMode !== void 0) return connection.credentialMode;
|
|
@@ -1626,4 +2522,4 @@ function requireIdentifier(type, value, label) {
|
|
|
1626
2522
|
return value;
|
|
1627
2523
|
}
|
|
1628
2524
|
//#endregion
|
|
1629
|
-
export {
|
|
2525
|
+
export { isDatabaseType as C, defaultDatabasePort as S, clientsSchema as _, parseStructuredQueryOutput as a, DATABASE_TYPES as b, DEFAULT_CATALOG_MAX_RESULT_CHARS as c, DEFAULT_CONNECT_TIMEOUT_MS as d, DEFAULT_MAX_QUERY_CHARS as f, classifyStatement as g, DEFAULT_QUERY_TIMEOUT_MS as h, validatePasswordRef as i, DEFAULT_CATALOG_MAX_TEXT_CHARS as l, DEFAULT_PRESET_ID as m, redactQueryResult as n, runClientQuery as o, DEFAULT_MAX_RESULT_CHARS as p, redactSecretText as r, DEFAULT_CATALOG_MAX_ASSETS as s, createConnectionService as t, DEFAULT_CATALOG_QUERY_TIMEOUT_MS as u, enforceReadRowLimit as v, databaseTypeLabel as x, assertSingleStatement as y };
|