@yejiming/dsh-data-agent 0.0.12 → 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.
Files changed (46) hide show
  1. package/README.en.md +56 -10
  2. package/README.md +56 -10
  3. package/conformance/dsh-ecosystem/baseline.json +59 -0
  4. package/conformance/dsh-ecosystem/dependencies.json +41 -0
  5. package/conformance/dsh-ecosystem/fixtures/host-degraded.fixture.json +12 -0
  6. package/conformance/dsh-ecosystem/fixtures/host-eligible.fixture.json +13 -0
  7. package/conformance/dsh-ecosystem/fixtures/host-rejected.fixture.json +12 -0
  8. package/conformance/dsh-ecosystem/fixtures/profiles/native-only/package.json +8 -0
  9. package/conformance/dsh-ecosystem/fixtures/profiles/native-plus-adapter/package.json +9 -0
  10. package/conformance/dsh-ecosystem/inventory.json +77 -0
  11. package/conformance/dsh-ecosystem/restrictions.json +20 -0
  12. package/dsh-plugin.json +67 -0
  13. package/lib/client.js +1348 -129
  14. package/lib/client.js.map +1 -1
  15. package/lib/{command-DuCpwVbl.js → command-utC5MHd9.js} +101 -60
  16. package/lib/command.js +1 -1
  17. package/lib/{connections-5sfdEDsG.js → connections-CHY4uB6z.js} +747 -65
  18. package/lib/defaults-Cngd8Tf8.js +131 -0
  19. package/lib/ecosystem.js +19 -0
  20. package/lib/index.js +30 -22
  21. package/lib/routes.js +5 -6
  22. package/lib/{tool-DgL0fBfj.js → tool-ZTOS4B33.js} +5 -167
  23. package/lib/tool.js +1 -1
  24. package/lib/types/client/DataAgentWorkbench.d.ts +1 -2
  25. package/lib/types/client/QueryResultTable.d.ts +13 -0
  26. package/lib/types/client/locales.d.ts +48 -0
  27. package/lib/types/client/persistence.d.ts +3 -1
  28. package/lib/types/client/query-export.d.ts +11 -0
  29. package/lib/types/client-discovery.d.ts +3 -4
  30. package/lib/types/clients.d.ts +14 -8
  31. package/lib/types/command.d.ts +2 -2
  32. package/lib/types/connections.d.ts +33 -4
  33. package/lib/types/database-types.d.ts +23 -0
  34. package/lib/types/defaults.d.ts +4 -0
  35. package/lib/types/ecosystem.d.ts +13 -0
  36. package/lib/types/index.d.ts +16 -10
  37. package/lib/types/query.d.ts +13 -11
  38. package/lib/types/sql.d.ts +9 -1
  39. package/lib/types/storage.d.ts +11 -1
  40. package/lib/types/structured-read.d.ts +2 -2
  41. package/lib/types/structured.d.ts +1 -1
  42. package/lib/types/tool.d.ts +5 -5
  43. package/lib/types/tui-connection-form.d.ts +10 -7
  44. package/package.json +27 -3
  45. package/preset/data-agent/agent.cordis.yml +4 -1
  46. package/lib/defaults-DP4RyRh1.js +0 -21
@@ -1,9 +1,10 @@
1
- import "./defaults-DP4RyRh1.js";
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 top-level LIMIT, not just a prompt.
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
- switch (tokenMatch[0].toLowerCase()) {
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 the whole `clients` config object (any type key). */
604
- const clientsSchema = z.dict(clientConfigSchema).default({});
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: ["--batch", "--raw"],
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: ["--batch", "--raw"],
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: ["--batch", "--raw"],
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
- /** Default ports when the connection does not carry one. */
633
- const DEFAULT_PORTS = {
634
- mysql: 3306,
635
- postgres: 5432,
636
- sqlite: 0,
637
- oracle: 1521,
638
- hive: 1e4,
639
- impala: 21050
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": return [
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 ?? DEFAULT_PORTS.mysql),
864
+ String(connection.port ?? defaultDatabasePort(type)),
663
865
  "-u",
664
- connection.user ?? "root",
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 ?? DEFAULT_PORTS.postgres),
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 ?? DEFAULT_PORTS.impala}`,
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": return { MYSQL_PWD: password };
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 ?? DEFAULT_PORTS.oracle}/${connection.database}` : ""
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 ?? DEFAULT_PORTS.hive}/${connection.database} ${connection.user} ${connection.password ?? ""}\n` : "";
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": return "";
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 ?? DEFAULT_PORTS.oracle}/${connection.database}` : ""
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": return `SHOW TABLES FROM \`${connection?.database ?? ""}\`;`;
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": return "SHOW DATABASES;";
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": return `SHOW TABLES FROM ${sanitizeIdentifier(type, schema)};`;
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": return `DESCRIBE ${sanitizeIdentifier(type, schema)}.${sanitizeIdentifier(type, table)};`;
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("\n");
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 CLI client. The SQL is written to the
1197
- * child's stdin (`{ data }` batch disposition) so it never appears in argv;
1198
- * passwords travel in the env entries built by the template.
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
- if (input.host !== void 0 && input.host.length > 0) connection.host = input.host;
1313
- if (input.port !== void 0) connection.port = input.port;
1314
- if (input.user !== void 0 && input.user.length > 0) connection.user = input.user;
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: resolvedOptions.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(void 0, connect), signal, introspection), connection);
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 persistAtomically = async (sessionId, profileId, profile) => {
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
- return draft === void 0 ? void 0 : copyFormDraft(draft);
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
- await persistAtomically(sessionId, profileId, profileFromConnection(normalized, updatedAt));
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
- return parseListing(connection.type, stdout).slice(0, resolvedOptions.introspectMaxTables);
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 isDatabaseType(value) {
1574
- return value === "mysql" || value === "postgres" || value === "sqlite" || value === "oracle" || value === "hive" || value === "impala";
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 { classifyStatement as a, assertSingleStatement as c, runClientQuery as i, redactQueryResult as n, clientsSchema as o, redactSecretText as r, enforceReadRowLimit as s, createConnectionService as t };
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 };