@nsyan/db 1.2.1 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nsyan/db",
3
- "version": "1.2.1",
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
  }
package/src/core/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // core/index.ts —— core 聚合 re-export(package.json exports "./core" 的入口)
2
2
  export type { DbTypeId, DbFamily, ConnConfig, ParsedTarget, DbConnection, ExecOpts,
3
- CandidateStatus, Candidate, TableInfo, ColumnInfo, QueryResult, ListTablesResult,
3
+ CandidateStatus, Candidate, CandidateInput, TableInfo, ColumnInfo, QueryResult, ListTablesResult,
4
4
  DescribeTableResult, TestConnectionResult } from "./types.js";
5
5
  export { stripComments, splitStatements, isWriteStatement, isDropStatement } from "./sql-text.js";
6
6
  export { decide } from "./policy.js";
@@ -0,0 +1,34 @@
1
+ // scan/tree.ts —— 项目文件树收集(AI 扫描时代的"发现层")
2
+ // 设计(v1.3.0 共识):文件发现与内容提取全部交给会话模型——本模块只做确定性的事情:
3
+ // 走完整棵目录树、排除无关目录、产出相对路径清单。语言无关(Java/Py/TS/Go/Rust...)。
4
+ // 注:不再做任何"这是不是配置文件"的猜测(原文件名模式匹配已废弃)——
5
+ // 模型看到完整树后自行判断哪些文件值得读(用其自带 read 工具),长尾格式天然覆盖。
6
+
7
+ import { resolveRoot, walk } from "./walker.js";
8
+
9
+ export interface TreeResult {
10
+ root: string; // 绝对路径
11
+ total: number; // 收集到的文件总数
12
+ truncated: boolean; // 是否超出 MAX_TREE_LINES 被截断
13
+ lines: string[]; // 相对路径清单(已排序:浅层优先)
14
+ }
15
+
16
+ const MAX_TREE_LINES = 3000;
17
+
18
+ /** 收集项目文件树(相对路径排序清单);越界由 resolveRoot 拒绝 */
19
+ export function collectTree(rootInput: string): TreeResult {
20
+ const root = resolveRoot(rootInput);
21
+ const files = walk(root);
22
+ const rels = files
23
+ .map((f) => f.slice(root.length + 1).split("\\").join("/"))
24
+ .sort((a, b) => {
25
+ const da = a.split("/").length, db = b.split("/").length;
26
+ return da !== db ? da - db : a.localeCompare(b);
27
+ });
28
+ return {
29
+ root,
30
+ total: rels.length,
31
+ truncated: rels.length > MAX_TREE_LINES,
32
+ lines: rels.slice(0, MAX_TREE_LINES),
33
+ };
34
+ }
@@ -0,0 +1,119 @@
1
+ // scan/validate.ts —— AI 提交候选的校验层(防幻觉 + 归一化,纯函数可单测)
2
+ // 设计(v1.3.0 共识):候选由会话模型从配置文件提取后经 db_scan_save 提交。
3
+ // 模型可能编造 host/类型,这里用确定性规则过滤:
4
+ // ① dialectId 必须在 registry ② 带 url 时方言 parseUrl 必须能解析(claim 与 url 矛盾即拒)
5
+ // ③ 无 url 时 host 必填、port 范围校验 ④ REQUIRED 表判定缺字段 ⑤ 同名查重
6
+
7
+ import { basename } from "node:path";
8
+ import { registry } from "../../dialects/index.js";
9
+ import type { Candidate, CandidateInput, CandidateStatus, ConnConfig, DbTypeId } from "../types.js";
10
+
11
+ /** 各类型建连必需字段(空串同样视为缺失)——家族语义,非正则,保留 */
12
+ const REQUIRED: Record<DbTypeId, string[]> = {
13
+ postgresql: ["host", "port", "database", "username", "password"],
14
+ mysql: ["host", "port", "database", "username", "password"],
15
+ oracle: ["host", "port", "database", "username", "password"],
16
+ dm: ["host", "port", "database", "username", "password"],
17
+ hive: ["host", "port", "database", "username"],
18
+ spark: ["host", "port", "database", "username"],
19
+ redis: ["host", "port", "password"],
20
+ elasticsearch: ["host", "port"],
21
+ neo4j: ["host", "port", "username", "password"], // 工作库可选(缺省 neo4j;社区版默认开认证)
22
+ mongodb: ["host", "port"], // 账号/工作库可选(本地无认证常见)
23
+ };
24
+
25
+ function nonEmpty(v: unknown): v is string | number {
26
+ return v !== undefined && v !== null && String(v).trim() !== "";
27
+ }
28
+
29
+ export interface ValidateResult {
30
+ candidates: Candidate[];
31
+ rejected: string[]; // 被拒候选及原因(回传给模型可自查重提)
32
+ }
33
+
34
+ /** 校验 + 归一化一批模型提交的候选 */
35
+ export function validateCandidates(raw: unknown, existingNames: Set<string>): ValidateResult {
36
+ const list = Array.isArray(raw) ? raw : [raw];
37
+ const candidates: Candidate[] = [];
38
+ const rejected: string[] = [];
39
+ const seenNames = new Set(existingNames);
40
+
41
+ for (const [i, item] of list.entries()) {
42
+ if (item === null || typeof item !== "object") {
43
+ rejected.push(`#${i + 1}: 非对象,已丢弃`);
44
+ continue;
45
+ }
46
+ const c = item as Record<string, unknown> & Partial<CandidateInput>;
47
+
48
+ // ① 类型必须被注册表认领
49
+ const dialect = registry.get(c.dialectId as DbTypeId);
50
+ if (!dialect) {
51
+ rejected.push(`#${i + 1}: 未知数据库类型 ${JSON.stringify(c.dialectId)},支持: ${[...registry.keys()].join("/")}`);
52
+ continue;
53
+ }
54
+
55
+ // ② 带 url 时:方言 parseUrl 必须能解析(防幻觉——claim 与 url 矛盾整条拒)
56
+ // 解析成功时以解析结果为准(host/port/database/username/password 由 URL 补全)
57
+ // 归一化后 port/dbIndex 恒为 number(下行 parseInt 收敛)。此处用 Partial<ConnConfig> 而非
58
+ // Partial<CandidateInput>——后者的 port 是 number|string,会让下方端口范围校验退化为字符串比较。
59
+ let bag: Partial<ConnConfig> = {
60
+ host: typeof c.host === "string" ? c.host.trim() : undefined,
61
+ port: typeof c.port === "number" ? c.port : parseInt(String(c.port ?? ""), 10) || undefined,
62
+ username: nonEmpty(c.username) ? String(c.username) : undefined,
63
+ password: nonEmpty(c.password) ? String(c.password) : undefined,
64
+ database: nonEmpty(c.database) ? String(c.database) : undefined,
65
+ dbIndex: typeof c.dbIndex === "number" ? c.dbIndex : parseInt(String(c.dbIndex ?? ""), 10) || undefined,
66
+ };
67
+ if (nonEmpty(c.url)) {
68
+ const parsed = dialect.parseUrl(String(c.url).trim());
69
+ if (!parsed) {
70
+ rejected.push(`#${i + 1}: url 无法被 ${dialect.id} 方言解析(疑似编造),已拒绝: ${String(c.url).slice(0, 80)}`);
71
+ continue;
72
+ }
73
+ bag = {
74
+ ...bag,
75
+ host: parsed.host,
76
+ port: parsed.port,
77
+ username: bag.username ?? parsed.username,
78
+ password: bag.password ?? parsed.password,
79
+ database: bag.database ?? parsed.database,
80
+ dbIndex: bag.dbIndex ?? parsed.dbIndex,
81
+ };
82
+ }
83
+
84
+ // ③ 无 url 时 host 必填;port 范围校验
85
+ if (!nonEmpty(bag.host)) {
86
+ rejected.push(`#${i + 1}: ${dialect.id} 候选缺 host(无 url 时 host 必填)`);
87
+ continue;
88
+ }
89
+ if (bag.port !== undefined && (!Number.isInteger(bag.port) || bag.port < 1 || bag.port > 65535)) {
90
+ rejected.push(`#${i + 1}: ${dialect.id} 候选 port 非法: ${bag.port}`);
91
+ continue;
92
+ }
93
+
94
+ // ④ 缺字段判定
95
+ const missing = (REQUIRED[dialect.id] ?? []).filter((f) => !nonEmpty(bag[f as keyof ConnConfig]));
96
+
97
+ // ⑤ 命名:显式 name > 默认;同名(含与本批前序候选撞名)→ exists 状态
98
+ let name = nonEmpty(c.name) ? String(c.name).trim()
99
+ : `${dialect.id}-${bag.host}${bag.database ? "-" + bag.database : ""}`;
100
+ let status: CandidateStatus = missing.length > 0 ? "incomplete" : "ready";
101
+ if (seenNames.has(name)) status = "exists";
102
+ seenNames.add(name);
103
+
104
+ candidates.push({
105
+ status,
106
+ dialectId: dialect.id,
107
+ partial: { name, ...bag } as Partial<import("../types.js").ConnConfig>,
108
+ missing,
109
+ source: nonEmpty(c.source) ? String(c.source) : "AI 提取",
110
+ warnings: Array.isArray(c.warnings) ? c.warnings.map(String).slice(0, 3) : undefined,
111
+ });
112
+ }
113
+ return { candidates, rejected };
114
+ }
115
+
116
+ /** 供展示层使用的默认项目名(db_scan_save 的 name 兜底) */
117
+ export function defaultProjectName(root: string): string {
118
+ return basename(root);
119
+ }
package/src/core/types.ts CHANGED
@@ -37,14 +37,28 @@ export interface ParsedTarget {
37
37
  export interface DbConnection { type: DbTypeId; client: unknown; close(): Promise<void>; }
38
38
  export interface ExecOpts { readonly: boolean; maxRows: number; timeoutSec: number; }
39
39
 
40
- // scan/candidates.ts
41
- export type CandidateStatus = "ready" | "incomplete" | "encrypted" | "exists";
40
+ // scan(v1.3.0:文件发现与提取交给会话模型,本层只做校验归一化)
41
+ export type CandidateStatus = "ready" | "incomplete" | "exists";
42
+ /** 模型经 db_scan_save 提交的原始候选形状 */
43
+ export interface CandidateInput {
44
+ dialectId: DbTypeId;
45
+ host?: string; port?: number | string;
46
+ username?: string; password?: string;
47
+ database?: string; dbIndex?: number | string;
48
+ /** 完整连接串(可选;提供时必须能被方言 parseUrl 解析,否则整条拒绝——防幻觉) */
49
+ url?: string;
50
+ name?: string;
51
+ /** 来源描述(如配置文件相对路径),展示用 */
52
+ source?: string;
53
+ warnings?: string[];
54
+ }
42
55
  export interface Candidate {
43
56
  status: CandidateStatus;
44
- dialectId: DbTypeId; // 由方言 fingerprints 匹配得出
45
- partial: Partial<ConnConfig>; // 已抽到的字段
57
+ dialectId: DbTypeId;
58
+ partial: Partial<ConnConfig>; // 归一化后的字段(含默认 name)
46
59
  missing: string[]; // 待补字段名(incomplete 时)
47
- source: { file: string; profile?: string; confidence: number };
60
+ source: string; // 来源描述
61
+ warnings?: string[];
48
62
  }
49
63
 
50
64
  // ── 以下从 src/db.ts 原样搬入(字段不变) ──────────
@@ -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> {
@@ -7,19 +7,27 @@ import type { ConnConfig, DbConnection, ExecOpts, ParsedTarget,
7
7
  import { SearchDialect, type DslKind } from "./search-dialect.js";
8
8
  import { register, filterTables, type Fingerprints } from "./dialect.js";
9
9
 
10
- // ── URL 解析:http(s)://host:port(认证走账号/密码字段,API Key 二期)───
11
- const ES_RE = /^(https?):\/\/([^:/?#@]+)(?::(\d+))?(\/.*)?$/;
10
+ // ── URL 解析:http(s)://[user:pass@]host:port(认证解出后归一化到 username/password,API Key 二期)───
11
+ const ES_RE = /^(https?):\/\/(?:([^:/?#@]*)(?::([^@]*))?@)?([^:/?#@]+)(?::(\d+))?(\/.*)?$/;
12
12
 
13
13
  function parseEsUrl(url: string): ParsedTarget | null {
14
14
  const clean = url.split("?")[0].replace(/\/+$/, "");
15
15
  const m = clean.match(ES_RE);
16
16
  if (!m) return null;
17
- const host = m[2];
17
+ const host = m[4];
18
18
  const defaultPort = m[1] === "https" ? 443 : 9200;
19
- const port = m[3] ? parseInt(m[3], 10) : defaultPort;
19
+ const port = m[5] ? parseInt(m[5], 10) : defaultPort;
20
20
  const ssl = m[1] === "https";
21
21
  if (!host) return null;
22
- return { host, port, ssl };
22
+ const out: ParsedTarget = { host, port, ssl };
23
+ // userinfo 段(可能含 URL 编码密码)解出归一化,供扫描建连/测连直接用
24
+ if (m[2] !== undefined) {
25
+ try { out.username = decodeURIComponent(m[2]); } catch { out.username = m[2]; }
26
+ }
27
+ if (m[3] !== undefined) {
28
+ try { out.password = decodeURIComponent(m[3]); } catch { out.password = m[3]; }
29
+ }
30
+ return out;
23
31
  }
24
32
 
25
33
  /** 从 `version.number`(如 "7.17.0")取大版本号 */
@@ -155,7 +163,9 @@ class ElasticsearchDialect extends SearchDialect {
155
163
  return { columns: ["count"], rows: [[count]], rowCount: count };
156
164
  }
157
165
  if (kind.endpoint === "_mget") {
158
- 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]);
159
169
  return docsToRows(unwrap(res));
160
170
  }
161
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(),