@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 +38 -26
- package/package.json +7 -4
- package/src/config.ts +18 -3
- package/src/core/scan/validate.ts +5 -3
- package/src/dialects/dm.ts +5 -2
- package/src/dialects/elasticsearch.ts +3 -1
- package/src/dialects/hive.ts +3 -1
- package/src/dialects/mysql.ts +4 -2
- package/src/dialects/neo4j.ts +2 -2
- package/src/dialects/oracle.ts +4 -2
- package/src/dialects/redis.ts +8 -3
- package/src/dialects/spark.ts +2 -1
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
|
-
//
|
|
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
|
-
// ── 注册
|
|
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.
|
|
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
|
|
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
|
|
294
|
+
return TYPE_LABELS[type]?.short ?? type;
|
|
280
295
|
}
|
|
281
296
|
|
|
282
297
|
export function fullTypeLabel(type: DbTypeId): string {
|
|
283
|
-
return
|
|
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
|
-
|
|
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
|
|
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()
|
package/src/dialects/dm.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
package/src/dialects/hive.ts
CHANGED
|
@@ -61,7 +61,9 @@ class HiveDialect extends BigDataDialect {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
protected async doConnect(config: ConnConfig, timeoutMs: number): Promise<DbConnection> {
|
|
64
|
-
|
|
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(),
|
package/src/dialects/mysql.ts
CHANGED
|
@@ -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) {
|
package/src/dialects/neo4j.ts
CHANGED
|
@@ -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
|
|
package/src/dialects/oracle.ts
CHANGED
|
@@ -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
|
-
|
|
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",
|
package/src/dialects/redis.ts
CHANGED
|
@@ -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
|
|
96
|
+
const scanPattern = pattern ? pattern.split("").map((ch) => ch === "%" ? "*" : ch === "_" ? "?" : ch).join("") : null;
|
|
97
97
|
do {
|
|
98
|
-
|
|
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
|
-
|
|
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 [
|
package/src/dialects/spark.ts
CHANGED
|
@@ -58,7 +58,8 @@ class SparkDialect extends BigDataDialect {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
protected async doConnect(config: ConnConfig, timeoutMs: number): Promise<DbConnection> {
|
|
61
|
-
|
|
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(),
|