@nsyan/db 1.3.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -50,13 +50,21 @@ function buildDisplayList(configs: ConnConfig[]): { list: string[]; map: Map<str
50
50
  // ── 构建「可用数据库」提示(注入系统提示,让 AI 知道有哪些连接) ────
51
51
  // 效率项(Spec §6):每连接只注一行 `名称[家族] + 一行语义`,细节压进系统提示
52
52
 
53
- // 各家族一行语义(KV/搜索/大数据行由后续任务的 dialect.label 接管,此处三库先行)
53
+ // 各家族一行语义:让 AI 一眼知道该类型的 sql 参数填什么(覆盖全部十种类型)
54
54
  function familyHint(c: ConnConfig): string {
55
55
  switch (c.type) {
56
56
  case "postgresql":
57
57
  case "mysql":
58
58
  case "oracle":
59
+ case "dm":
59
60
  return "关系型,sql 参数填 SQL";
61
+ case "redis":
62
+ return "Redis 键值库,sql 参数填单条 Redis 命令(如 GET key / SCAN 0 MATCH user:* COUNT 100;KEYS 已禁用,扫描请用 SCAN)";
63
+ case "elasticsearch":
64
+ return "Elasticsearch 搜索库,sql 参数填 DSL JSON(如 {\"query\":{\"match_all\":{}}};仅支持读端点 _search/_count/_mget)";
65
+ case "hive":
66
+ case "spark":
67
+ return "大数据 SQL 引擎,sql 参数填 SQL(如 SELECT * FROM 库.表 LIMIT 10)";
60
68
  case "mongodb":
61
69
  return "MongoDB 文档库,sql 参数填 JSON 命令信封(如 {\"find\":\"users\",\"filter\":{}};读命令 find/count/distinct/aggregate)";
62
70
  case "neo4j":
@@ -655,7 +663,7 @@ export default function (pi: ExtensionAPI) {
655
663
  const val = await ctx.ui.select(`选择 ${field.label}`, field.options);
656
664
  if (!val) continue;
657
665
  if (field.valueMap) {
658
- (newCfg as any)[field.key] = field.valueMap[val];
666
+ (newCfg as any)[field.key] = (field.valueMap as Record<string, unknown>)[val];
659
667
  } else {
660
668
  (newCfg as any)[field.key] = val === "是";
661
669
  }
@@ -699,7 +707,11 @@ export default function (pi: ExtensionAPI) {
699
707
  };
700
708
  });
701
709
 
702
- // ── 注册 3 个工具(给 LLM 调用) ──────────────────
710
+ // ── 注册 6 个工具(给 LLM 调用) ──────────────────
711
+ // 注:pi 的 AgentToolResult.details 为必填字段(registerTool 的 TDetails 默认 unknown)。
712
+ // 本插件工具没有结构化详情,故在返回对象里统一显式写 details: undefined——与"完全不提供该字段"
713
+ // 在运行时等价:宿主读取走可选链(result.details?.x)、合并判定用 `!== undefined`,
714
+ // 且 JSON 序列化会跳过 undefined 值。
703
715
 
704
716
  // 解析工具 database 参数:缺省走默认连接(Spec §11.1);无默认则报错指引
705
717
  function resolveTargetDb(name: string | undefined): { config?: ConnConfig; error?: string } {
@@ -735,7 +747,7 @@ export default function (pi: ExtensionAPI) {
735
747
 
736
748
  const target = resolveTargetDb(params.database);
737
749
  if (target.error || !target.config) {
738
- return {
750
+ return { details: undefined,
739
751
  content: [{ type: "text" as const, text: target.error ?? "未找到数据库配置" }],
740
752
  };
741
753
  }
@@ -748,7 +760,7 @@ export default function (pi: ExtensionAPI) {
748
760
  // verdict = dialect.isAllowed(sql, readonly) → decide → deny/confirm/run
749
761
  const dialect = registry.get(config.type);
750
762
  if (!dialect) {
751
- return {
763
+ return { details: undefined,
752
764
  content: [{ type: "text" as const, text: `数据库类型 "${config.type}" 暂不支持。` }],
753
765
  };
754
766
  }
@@ -758,19 +770,19 @@ export default function (pi: ExtensionAPI) {
758
770
  const note = !cfg.ai_readonly && config.forceReadonly === true
759
771
  ? `(连接 ${config.name} 已设置强制只读)`
760
772
  : "";
761
- return {
773
+ return { details: undefined,
762
774
  content: [{ type: "text" as const, text: (verdict.reason ?? "该操作不被允许。如需修改,请执行 /db config 更改配置。") + note }],
763
775
  };
764
776
  }
765
777
  // 写操作强制附执行理由(v1.1 UX 共识 Q1/Q3:与确认策略解耦;缺 reason 拒绝并引导 AI 补充)
766
778
  if (writeRequiresReason(verdict, params.reason)) {
767
- return {
779
+ return { details: undefined,
768
780
  content: [{ type: "text" as const, text: "写操作必须附执行理由:请在 reason 参数中说明动机与影响范围(如\"将status=2的历史订单归档,预计影响1.2万行\"),补充后重试。" }],
769
781
  };
770
782
  }
771
783
  if (action === "confirm") {
772
784
  if (!ctx?.hasUI) {
773
- return {
785
+ return { details: undefined,
774
786
  content: [{ type: "text" as const, text: "当前环境无法弹出确认对话框,已取消 SQL 执行。请在有界面的环境中操作。" }],
775
787
  };
776
788
  }
@@ -779,7 +791,7 @@ export default function (pi: ExtensionAPI) {
779
791
  `理由: ${params.reason}\n\n${verdict.summary ?? ""}\n\n数据库: ${config.name}\n\nSQL:\n${params.sql}`,
780
792
  );
781
793
  if (!ok) {
782
- return {
794
+ return { details: undefined,
783
795
  content: [{ type: "text" as const, text: "用户取消了 SQL 执行。" }],
784
796
  };
785
797
  }
@@ -793,7 +805,7 @@ export default function (pi: ExtensionAPI) {
793
805
  );
794
806
 
795
807
  if (!result.success) {
796
- return {
808
+ return { details: undefined,
797
809
  content: [{ type: "text" as const, text: `查询失败: ${result.error}` }],
798
810
  };
799
811
  }
@@ -841,7 +853,7 @@ export default function (pi: ExtensionAPI) {
841
853
  text += `\n执行理由: ${params.reason}`;
842
854
  }
843
855
 
844
- return { content: [{ type: "text" as const, text }] };
856
+ return { details: undefined, content: [{ type: "text" as const, text }] };
845
857
  },
846
858
  });
847
859
 
@@ -858,7 +870,7 @@ export default function (pi: ExtensionAPI) {
858
870
  async execute(_toolCallId: string, params: { database?: string; pattern?: string }, _signal: any) {
859
871
  const target = resolveTargetDb(params.database);
860
872
  if (target.error || !target.config) {
861
- return {
873
+ return { details: undefined,
862
874
  content: [{ type: "text" as const, text: target.error ?? "未找到数据库配置" }],
863
875
  };
864
876
  }
@@ -866,14 +878,14 @@ export default function (pi: ExtensionAPI) {
866
878
 
867
879
  const dialect = registry.get(config.type);
868
880
  if (!dialect) {
869
- return {
881
+ return { details: undefined,
870
882
  content: [{ type: "text" as const, text: `数据库类型 "${config.type}" 暂不支持。` }],
871
883
  };
872
884
  }
873
885
  const result = await dialect.listTables(toRuntimeConfig(config, dialect.defaultPort), params.pattern);
874
886
 
875
887
  if (!result.success || !result.tables) {
876
- return {
888
+ return { details: undefined,
877
889
  content: [{ type: "text" as const, text: `获取表列表失败: ${result.error}` }],
878
890
  };
879
891
  }
@@ -885,7 +897,7 @@ export default function (pi: ExtensionAPI) {
885
897
  return `${schema}${t.name} (${t.type})${desc}`;
886
898
  });
887
899
 
888
- return {
900
+ return { details: undefined,
889
901
  content: [{ type: "text" as const, text: `数据库 "${config.name}"${params.pattern ? `(pattern: ${params.pattern})` : ""} 共 ${result.count} 张表:\n${lines.join("\n")}` }],
890
902
  };
891
903
  },
@@ -904,7 +916,7 @@ export default function (pi: ExtensionAPI) {
904
916
  async execute(_toolCallId: string, params: { database?: string; table: string }, _signal: any) {
905
917
  const target = resolveTargetDb(params.database);
906
918
  if (target.error || !target.config) {
907
- return {
919
+ return { details: undefined,
908
920
  content: [{ type: "text" as const, text: target.error ?? "未找到数据库配置" }],
909
921
  };
910
922
  }
@@ -912,14 +924,14 @@ export default function (pi: ExtensionAPI) {
912
924
 
913
925
  const dialect = registry.get(config.type);
914
926
  if (!dialect) {
915
- return {
927
+ return { details: undefined,
916
928
  content: [{ type: "text" as const, text: `数据库类型 "${config.type}" 暂不支持。` }],
917
929
  };
918
930
  }
919
931
  const result = await dialect.describeTable(toRuntimeConfig(config, dialect.defaultPort), params.table);
920
932
 
921
933
  if (!result.success || !result.columns) {
922
- return {
934
+ return { details: undefined,
923
935
  content: [{ type: "text" as const, text: `获取表结构失败: ${result.error}` }],
924
936
  };
925
937
  }
@@ -935,7 +947,7 @@ export default function (pi: ExtensionAPI) {
935
947
  );
936
948
  }
937
949
 
938
- return {
950
+ return { details: undefined,
939
951
  content: [{ type: "text" as const, text: lines.join("\n") }],
940
952
  };
941
953
  },
@@ -966,9 +978,9 @@ export default function (pi: ExtensionAPI) {
966
978
  "4. 调用 db_scan_save 提交候选数组。带 url 的候选必须能被对应方言解析,编造的 url 会被拒绝;",
967
979
  "5. 工具会弹确认框让用户逐个确认并补录密码,把工具返回的保存结果汇报给用户。",
968
980
  ].join("\n");
969
- return { content: [{ type: "text" as const, text }] };
981
+ return { details: undefined, content: [{ type: "text" as const, text }] };
970
982
  } catch (err) {
971
- return {
983
+ return { details: undefined,
972
984
  content: [{ type: "text" as const, text: `扫描失败: ${err instanceof Error ? err.message : String(err)}` }],
973
985
  };
974
986
  }
@@ -996,7 +1008,7 @@ export default function (pi: ExtensionAPI) {
996
1008
  }),
997
1009
  async execute(_toolCallId: string, params: { candidates: unknown[] }, _signal: any, _onUpdate?: any, ctx?: any) {
998
1010
  if (!ctx?.ui) {
999
- return { content: [{ type: "text" as const, text: "db_scan_save 需要交互式会话(ctx.ui 不可用)。请引导用户在终端执行 /db add 手动添加。" }] };
1011
+ return { details: undefined, content: [{ type: "text" as const, text: "db_scan_save 需要交互式会话(ctx.ui 不可用)。请引导用户在终端执行 /db add 手动添加。" }] };
1000
1012
  }
1001
1013
  const { candidates, rejected } = validateCandidates(params.candidates, new Set(loadConfigs().map((c) => c.name)));
1002
1014
  const lines: string[] = [];
@@ -1006,7 +1018,7 @@ export default function (pi: ExtensionAPI) {
1006
1018
  if (candidates.length === 0) {
1007
1019
  lines.push(rejected.length > 0 ? "没有可保存的候选。" : "未收到有效候选(candidates 需为数组,每项含 dialectId+host)。",
1008
1020
  "提示:从配置文件原文提取后重试;确认方言取值在支持列表内。");
1009
- return { content: [{ type: "text" as const, text: lines.join("\n") }] };
1021
+ return { details: undefined, content: [{ type: "text" as const, text: lines.join("\n") }] };
1010
1022
  }
1011
1023
  lines.push(`收到 ${candidates.length} 个连接候选,逐个确认:`, ...candidates.map(candidateLine), "");
1012
1024
  const results: string[] = [];
@@ -1019,7 +1031,7 @@ export default function (pi: ExtensionAPI) {
1019
1031
  }
1020
1032
  }
1021
1033
  lines.push("", "保存结果:", ...results.map((r) => " - " + r));
1022
- return { content: [{ type: "text" as const, text: lines.join("\n") }] };
1034
+ return { details: undefined, content: [{ type: "text" as const, text: lines.join("\n") }] };
1023
1035
  },
1024
1036
  });
1025
1037
 
@@ -1033,7 +1045,7 @@ export default function (pi: ExtensionAPI) {
1033
1045
  async execute() {
1034
1046
  const configs = loadConfigs();
1035
1047
  if (configs.length === 0) {
1036
- return {
1048
+ return { details: undefined,
1037
1049
  content: [{ type: "text" as const, text: "尚无数据库连接。请引导用户在终端执行 /db add 或 /db scan 建连。" }],
1038
1050
  };
1039
1051
  }
@@ -1046,7 +1058,7 @@ export default function (pi: ExtensionAPI) {
1046
1058
  const url = registry.get(c.type)?.displayUrl(c) ?? "";
1047
1059
  return `- ${c.name} [${shortTypeLabel(c.type)}]${marks ? " " + marks : ""} - ${fullTypeLabel(c.type)}${c.database ? " · 库: " + c.database : ""} · ${url} · ${test}${used ? " · " + used : ""}${c.description ? " · " + c.description : ""}`;
1048
1060
  });
1049
- return {
1061
+ return { details: undefined,
1050
1062
  content: [{ type: "text" as const, text: `共 ${configs.length} 个连接:\n${lines.join("\n")}\n\n标注 [prod·强制只读] 的连接永远只读(即使全局允许写操作)。` }],
1051
1063
  };
1052
1064
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nsyan/db",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "AI 数据库接入扩展 for pi —— 达梦(DM)/PostgreSQL/MySQL/Oracle/Redis/Elasticsearch/MongoDB/Neo4j/Hive/Spark 十种数据库,六大家族方言,提供查询/表结构/扫描建连/连接清单 LLM 工具 | Database access extension for the pi coding agent: query, schema browsing, connection scanning and audit tools for PostgreSQL, MySQL, Oracle, DM (Dameng 达梦), Redis, Elasticsearch, MongoDB, Neo4j, Hive and Spark",
5
5
  "keywords": [
6
6
  "pi-extension",
@@ -60,14 +60,17 @@
60
60
  "mysql2": "^3.23.1",
61
61
  "neo4j-driver": "^5.28.3",
62
62
  "oracledb": "^7.0.1",
63
- "neo4j-driver": "^5.28.3",
64
63
  "pg": "^8.22.0"
65
64
  },
66
65
  "devDependencies": {
66
+ "@earendil-works/pi-coding-agent": "^0.85.1",
67
+ "@types/node": "^22.20.2",
67
68
  "@types/pg": "^8.20.0",
68
- "tsx": "^4.23.13"
69
+ "tsx": "^4.23.13",
70
+ "typescript": "^7.0.2"
69
71
  },
70
72
  "scripts": {
71
- "test": "tsx --test \"test/**/*.test.ts\""
73
+ "test": "tsx --test \"test/**/*.test.ts\"",
74
+ "typecheck": "tsc -p tsconfig.typecheck.json"
72
75
  }
73
76
  }
package/src/config.ts CHANGED
@@ -274,11 +274,26 @@ export function toRuntimeConfig(c: ConnConfig, defaultPort: number): ConnConfig
274
274
  };
275
275
  }
276
276
 
277
- // 供 UI 层展示类型短标签(原 index.ts 内 5 处重复映射的收敛点之一)
277
+ // 供 UI 层展示类型标签(原 index.ts 内 5 处重复映射的收敛点之一)
278
+ // 单一数据源:短标签与全称同表维护。键类型用 Record<DbTypeId, ...> 而非 Record<string, ...>,
279
+ // 新增方言而漏补标签时会在类型检查阶段报错,不再静默 fallback 成原始 id。
280
+ const TYPE_LABELS: Record<DbTypeId, { short: string; full: string }> = {
281
+ postgresql: { short: "PG", full: "PostgreSQL" },
282
+ mysql: { short: "MySQL", full: "MySQL" },
283
+ oracle: { short: "Oracle", full: "Oracle" },
284
+ dm: { short: "DM", full: "达梦" },
285
+ redis: { short: "Redis", full: "Redis" },
286
+ elasticsearch: { short: "ES", full: "Elasticsearch" },
287
+ mongodb: { short: "MongoDB", full: "MongoDB" },
288
+ neo4j: { short: "Neo4j", full: "Neo4j" },
289
+ hive: { short: "Hive", full: "Hive" },
290
+ spark: { short: "Spark", full: "Spark" },
291
+ };
292
+
278
293
  export function shortTypeLabel(type: DbTypeId): string {
279
- return ({ postgresql: "PG", mysql: "MySQL", oracle: "Oracle", mongodb: "MongoDB", neo4j: "Neo4j" } as Record<string, string>)[type] ?? type;
294
+ return TYPE_LABELS[type]?.short ?? type;
280
295
  }
281
296
 
282
297
  export function fullTypeLabel(type: DbTypeId): string {
283
- return ({ postgresql: "PostgreSQL", mysql: "MySQL", oracle: "Oracle", mongodb: "MongoDB", neo4j: "Neo4j" } as Record<string, string>)[type] ?? type;
298
+ return TYPE_LABELS[type]?.full ?? type;
284
299
  }
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { basename } from "node:path";
8
8
  import { registry } from "../../dialects/index.js";
9
- import type { Candidate, CandidateInput, CandidateStatus, DbTypeId } from "../types.js";
9
+ import type { Candidate, CandidateInput, CandidateStatus, ConnConfig, DbTypeId } from "../types.js";
10
10
 
11
11
  /** 各类型建连必需字段(空串同样视为缺失)——家族语义,非正则,保留 */
12
12
  const REQUIRED: Record<DbTypeId, string[]> = {
@@ -54,7 +54,9 @@ export function validateCandidates(raw: unknown, existingNames: Set<string>): Va
54
54
 
55
55
  // ② 带 url 时:方言 parseUrl 必须能解析(防幻觉——claim 与 url 矛盾整条拒)
56
56
  // 解析成功时以解析结果为准(host/port/database/username/password 由 URL 补全)
57
- let bag: Partial<CandidateInput> = {
57
+ // 归一化后 port/dbIndex 恒为 number(下行 parseInt 收敛)。此处用 Partial<ConnConfig> 而非
58
+ // Partial<CandidateInput>——后者的 port 是 number|string,会让下方端口范围校验退化为字符串比较。
59
+ let bag: Partial<ConnConfig> = {
58
60
  host: typeof c.host === "string" ? c.host.trim() : undefined,
59
61
  port: typeof c.port === "number" ? c.port : parseInt(String(c.port ?? ""), 10) || undefined,
60
62
  username: nonEmpty(c.username) ? String(c.username) : undefined,
@@ -90,7 +92,7 @@ export function validateCandidates(raw: unknown, existingNames: Set<string>): Va
90
92
  }
91
93
 
92
94
  // ④ 缺字段判定
93
- const missing = (REQUIRED[dialect.id] ?? []).filter((f) => !nonEmpty(bag[f as keyof CandidateInput]));
95
+ const missing = (REQUIRED[dialect.id] ?? []).filter((f) => !nonEmpty(bag[f as keyof ConnConfig]));
94
96
 
95
97
  // ⑤ 命名:显式 name > 默认;同名(含与本批前序候选撞名)→ exists 状态
96
98
  let name = nonEmpty(c.name) ? String(c.name).trim()
@@ -80,10 +80,12 @@ class DmDialect extends RelationalDialect {
80
80
  protected async doExecute(client: unknown, stmt: string, opts: ExecOpts): Promise<{ columns: string[]; rows: unknown[][]; rowCount: number }> {
81
81
  const dmConn = client as dmdb.Connection;
82
82
  const maxRows = opts.maxRows;
83
+ // 上游 dmdb 的 ExecuteOptions 未声明 maxRows/fetchArraySize(运行时可接受或被忽略);
84
+ // 结果另有下方 .slice(0, maxRows) 兜底,故此处仅放宽类型断言,不改运行时行为。
83
85
  const res = await dmConn.execute(stmt, [], {
84
86
  maxRows,
85
87
  fetchArraySize: maxRows,
86
- });
88
+ } as dmdb.ExecuteOptions);
87
89
  if (res.metaData && res.metaData.length > 0) {
88
90
  const columns = res.metaData.map((m: any) => m.name);
89
91
  const rows = (res.rows ?? []).slice(0, maxRows).map((r: any) => [...r]);
@@ -95,7 +97,8 @@ class DmDialect extends RelationalDialect {
95
97
  async versionQuery(conn: DbConnection): Promise<string> {
96
98
  const dmConn = conn.client as dmdb.Connection;
97
99
  const res = await dmConn.execute("SELECT * FROM V$VERSION");
98
- return (res.rows ?? [])[0]?.[0] as string ?? "unknown";
100
+ const rows = (res.rows as unknown[][] | undefined) ?? [];
101
+ return (rows[0]?.[0] as string | undefined) ?? "unknown";
99
102
  }
100
103
 
101
104
  async listTables(config: ConnConfig, pattern?: string): Promise<ListTablesResult> {
@@ -163,7 +163,9 @@ class ElasticsearchDialect extends SearchDialect {
163
163
  return { columns: ["count"], rows: [[count]], rowCount: count };
164
164
  }
165
165
  if (kind.endpoint === "_mget") {
166
- const res = await es.mget({ index, body: { docs: [] } });
166
+ // 注:_mget 需兼容 v7 客户端的 body 形态(本文件为 v7/v8/v9 三客户端分发),
167
+ // 而 v8/v9 的类型已把请求体改为顶层 docs,故按运行时通用形态传参并放宽参数类型。
168
+ const res = await es.mget({ index, body: { docs: [] } } as unknown as Parameters<ClientV8["mget"]>[0]);
167
169
  return docsToRows(unwrap(res));
168
170
  }
169
171
  // _search:DSL 整体即 body
@@ -61,7 +61,9 @@ class HiveDialect extends BigDataDialect {
61
61
  }
62
62
 
63
63
  protected async doConnect(config: ConnConfig, timeoutMs: number): Promise<DbConnection> {
64
- const client = new hive.HiveClient(TCLIService, TCLIService_types);
64
+ // 上游 hive-driver TCLIServiceTypes 声明不完整(缺 25 个请求类型),与其运行时实际
65
+ // 使用的 thrift 定义不一致;仅放宽类型断言,运行时对象未做任何改动。
66
+ const client = new hive.HiveClient(TCLIService, TCLIService_types as any);
65
67
  await client.connect(
66
68
  { host: config.host ?? "localhost", port: config.port ?? DEFAULT_PORT },
67
69
  new hive.connections.TcpConnection(),
@@ -109,9 +109,11 @@ class MysqlDialect extends RelationalDialect {
109
109
  try {
110
110
  const tables = await this.withConnection(config, async (conn) => {
111
111
  const mysqlConn = conn.client as mysql.Connection;
112
+ // config.database 为可选(ConnConfig),而 mysql2 的 ExecuteValues 不接受 undefined 元素;
113
+ // 此处仅放宽类型断言,运行时参数与既有行为完全一致。
112
114
  const [rows] = await mysqlConn.execute(
113
115
  "SELECT TABLE_NAME, TABLE_TYPE, TABLE_COMMENT FROM information_schema.TABLES WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME",
114
- [config.database],
116
+ [config.database] as any[],
115
117
  );
116
118
  const all = (rows as any[]).map((r: any) => ({
117
119
  schema: "",
@@ -149,7 +151,7 @@ class MysqlDialect extends RelationalDialect {
149
151
  try {
150
152
  const [commentRows] = await mysqlConn.execute(
151
153
  "SELECT COLUMN_NAME, COLUMN_COMMENT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?",
152
- [config.database, table],
154
+ [config.database, table] as any[],
153
155
  );
154
156
  const commentMap = new Map((commentRows as any[]).map((r: any) => [r.COLUMN_NAME, r.COLUMN_COMMENT]));
155
157
  for (const col of cols) {
@@ -270,7 +270,7 @@ class Neo4jDialect extends GraphDialect {
270
270
  const f = r._fields ?? [];
271
271
  const types = f[3] as string[] | null;
272
272
  if (!Array.isArray(types) || !types.includes(graphName)) continue;
273
- indexLines.push(`${f[0]}(${f[1]}, ${f[5] ?? "online"}) ON ${isRel ? "rel" : "node"}(${types.join(":")}).(${(f[4] ?? []).join(",")})`);
273
+ indexLines.push(`${f[0]}(${f[1]}, ${f[5] ?? "online"}) ON ${isRel ? "rel" : "node"}(${types.join(":")}).(${((f[4] ?? []) as string[]).join(",")})`);
274
274
  }
275
275
  } catch { /* SHOW 失败降级 */ }
276
276
  try {
@@ -281,7 +281,7 @@ class Neo4jDialect extends GraphDialect {
281
281
  const types = f[3] as string[] | null;
282
282
  if (!Array.isArray(types) || !types.includes(graphName)) continue;
283
283
  for (const p of (f[4] ?? []) as string[]) if (String(f[1]).toUpperCase().includes("UNIQUE")) uniqueProps.add(p);
284
- indexLines.push(`${f[0]}(${f[1]}) ON ${types.join(":")}.(${(f[4] ?? []).join(",")})`);
284
+ indexLines.push(`${f[0]}(${f[1]}) ON ${types.join(":")}.(${((f[4] ?? []) as string[]).join(",")})`);
285
285
  }
286
286
  } catch { /* 降级 */ }
287
287
 
@@ -1,7 +1,7 @@
1
1
  // src/dialects/oracle.ts
2
2
  import oracledb from "oracledb";
3
3
  import type { ConnConfig, DbConnection, ExecOpts, ParsedTarget,
4
- ListTablesResult, DescribeTableResult, ColumnInfo, TestConnectionResult } from "../core/types.js";
4
+ ListTablesResult, DescribeTableResult, TableInfo, ColumnInfo, TestConnectionResult } from "../core/types.js";
5
5
  import { RelationalDialect } from "./relational-dialect.js";
6
6
  import { register, filterTables, type Fingerprints } from "./dialect.js";
7
7
 
@@ -107,7 +107,9 @@ class OracleDialect extends RelationalDialect {
107
107
  WHERE T.OWNER NOT IN ('SYS', 'SYSTEM', 'DBSNMP', 'XDB')
108
108
  ORDER BY T.OWNER, T.TABLE_NAME`,
109
109
  );
110
- const all = (res.rows ?? []).map((r: any) => ({
110
+ // 显式标注 TableInfo[]:oracledb 无类型声明,res.rows 退化为 any,若不标注则
111
+ // filterTables 的泛型会回退到约束 { name; schema? },丢失 type/description 两个字段。
112
+ const all: TableInfo[] = (res.rows ?? []).map((r: any) => ({
111
113
  schema: r[1],
112
114
  name: r[0],
113
115
  type: "TABLE",
@@ -93,9 +93,13 @@ class RedisDialect extends KvDialect {
93
93
  let sampled = 0;
94
94
  let cursor = "0";
95
95
  // LIKE(%/_)→ SCAN 通配(*/?),全局替换(String.replace 单次替换是 bug)
96
- const matchArgs = pattern ? ["MATCH", pattern.split("").map((ch) => ch === "%" ? "*" : ch === "_" ? "?" : ch).join("")] : [];
96
+ const scanPattern = pattern ? pattern.split("").map((ch) => ch === "%" ? "*" : ch === "_" ? "?" : ch).join("") : null;
97
97
  do {
98
- const [next, keys] = await redis.scan(cursor, "COUNT", 100, ...matchArgs);
98
+ // ioredis scan 重载要求 MATCH/COUNT 固定次序,不能靠 spread 拼参;
99
+ // Redis 的 SCAN 命令本身不区分参数顺序,故两个分支与原来的命令语义完全一致。
100
+ const [next, keys] = scanPattern
101
+ ? await redis.scan(cursor, "MATCH", scanPattern, "COUNT", 100)
102
+ : await redis.scan(cursor, "COUNT", 100);
99
103
  cursor = next;
100
104
  for (const key of keys) {
101
105
  if (sampled >= 200) break;
@@ -166,7 +170,8 @@ class RedisDialect extends KvDialect {
166
170
  } else if (type === "set") {
167
171
  preview = JSON.stringify(await redis.smembers(key)).slice(0, 200);
168
172
  } else if (type === "zset") {
169
- preview = JSON.stringify(await redis.zrange(key, 0, 9, "WITHSCORES")).slice(0, 200);
173
+ // ioredis 6 zrange 重载把 stop 声明为 string|Buffer(漏了 number);索引 "9" 与数字 9 等价。
174
+ preview = JSON.stringify(await redis.zrange(key, 0, "9", "WITHSCORES")).slice(0, 200);
170
175
  }
171
176
  } catch { /* ignore */ }
172
177
  return [
@@ -58,7 +58,8 @@ class SparkDialect extends BigDataDialect {
58
58
  }
59
59
 
60
60
  protected async doConnect(config: ConnConfig, timeoutMs: number): Promise<DbConnection> {
61
- const client = new hive.HiveClient(TCLIService, TCLIService_types);
61
+ // hive.ts:上游 hive-driver 的 TCLIServiceTypes 声明不完整,仅放宽类型断言,不动运行时。
62
+ const client = new hive.HiveClient(TCLIService, TCLIService_types as any);
62
63
  await client.connect(
63
64
  { host: config.host ?? "localhost", port: config.port ?? DEFAULT_PORT },
64
65
  new hive.connections.TcpConnection(),