@saichao2026/dsh-clickhouse 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +251 -2
- package/cordis.patch.yml +7 -0
- package/lib/clickhouse.d.ts +112 -0
- package/lib/clickhouse.js +329 -0
- package/lib/clickhouse.js.map +1 -0
- package/lib/config.d.ts +87 -0
- package/lib/config.js +224 -0
- package/lib/config.js.map +1 -0
- package/lib/errors.d.ts +29 -0
- package/lib/errors.js +80 -0
- package/lib/errors.js.map +1 -0
- package/lib/format.d.ts +17 -0
- package/lib/format.js +100 -0
- package/lib/format.js.map +1 -0
- package/lib/index.d.ts +34 -0
- package/lib/index.js +74 -0
- package/lib/index.js.map +1 -0
- package/lib/sql-guard.d.ts +64 -0
- package/lib/sql-guard.js +472 -0
- package/lib/sql-guard.js.map +1 -0
- package/lib/tools.d.ts +22 -0
- package/lib/tools.js +540 -0
- package/lib/tools.js.map +1 -0
- package/lib/types.d.ts +82 -0
- package/lib/types.js +13 -0
- package/lib/types.js.map +1 -0
- package/package.json +61 -4
- package/src/clickhouse.ts +385 -0
- package/src/config.ts +301 -0
- package/src/errors.ts +86 -0
- package/src/format.ts +112 -0
- package/src/index.ts +88 -0
- package/src/sql-guard.ts +541 -0
- package/src/tools.ts +587 -0
- package/src/types.ts +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 赛超
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,252 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-clickhouse
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
ClickHouse query tools for **DSH / DeepSeek Harness**: seven `clickhouse_*` tools that let an agent explore databases, tables and columns, then answer data questions with single, read-only SQL statements. Zero runtime dependencies (Node's built-in `fetch` only), so nothing has to resolve inside your profile.
|
|
4
|
+
|
|
5
|
+
- **Read-only by construction** — a client-side guard allows one statement whose leading keyword is `SELECT` / `WITH` / `SHOW` / `DESCRIBE` / `EXPLAIN` / `EXISTS`, rejects write keywords anywhere in the statement, and refuses `INTO OUTFILE`, `FORMAT`, `SETTINGS` and external table functions; every query also carries server-side `max_result_rows` (with `result_overflow_mode=break`), `max_execution_time` and `max_memory_usage`.
|
|
6
|
+
- **Built for data Q&A** — the tools are ordered the way a person actually works: list databases → list tables → describe a table → sample rows → run the query, plus a column-name search for when you don't know which table holds a concept.
|
|
7
|
+
- **Old-server friendly** — verified against ClickHouse 21.12, including the `system.tables` columns that changed after it, and it tells the model to backtick non-ASCII aliases instead of failing with a bare syntax error.
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
dsh plugin --profile <profile> add @saichao2026/dsh-clickhouse
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
配置、工具清单、只读护栏的范围、以及本机验证记录见下文。
|
|
14
|
+
|
|
15
|
+
## 两步上手(新用户)
|
|
16
|
+
|
|
17
|
+
1. **装**:DSH 里 **设置 → 插件**(插件市场)搜 `clickhouse` → 选 `dsh-clickhouse` → 安装。
|
|
18
|
+
命令行等价写法:`dsh plugin --profile <profile> add @saichao2026/dsh-clickhouse`
|
|
19
|
+
2. **启用 + 重启**:装完在市场里点「启用」,然后重启 DSH。
|
|
20
|
+
|
|
21
|
+
**连接已经内置,装完不用配任何东西。** 默认值是部署环境:`http://10.0.0.34:8123`、用户 `default`、默认库 `default`、只读。客户不会配环境,所以这组默认值直接写死在代码里(`src/config.ts` 的 `DEFAULT_CONFIG`),装完就能查。
|
|
22
|
+
|
|
23
|
+
只有要连**别的**服务器时才需要配置(写法详见「配置」一节):
|
|
24
|
+
|
|
25
|
+
```yaml
|
|
26
|
+
- id: clickhouse
|
|
27
|
+
name: @saichao2026/dsh-clickhouse
|
|
28
|
+
config: { url: http://<主机>:8123, user: default, password: '' }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
连不上时得到的是很明确的报错,而不是一句 `fetch failed`:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
无法连接 ClickHouse(http://10.0.0.34:8123):连接被拒绝。请确认服务已启动、端口是 HTTP 接口(默认 8123)而不是原生 TCP 端口(9000)。
|
|
35
|
+
插件已内置连接 http://10.0.0.34:8123(用户 default)。确认服务在跑还是连不上时,说明目标服务器不是它,请在 profile 的用户补丁层改:
|
|
36
|
+
~/.dsh/profiles/<profile>/cordis.patch.yml
|
|
37
|
+
- id: clickhouse
|
|
38
|
+
name: @saichao2026/dsh-clickhouse
|
|
39
|
+
config: { url: http://<主机>:8123, user: default, password: '' }
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 它是什么
|
|
43
|
+
|
|
44
|
+
给 **DSH / DeepSeek Harness** 用的 ClickHouse 数据问答插件:把「库表结构探查 + 带护栏的只读 SQL 执行」变成 agent 可以直接调用的工具,用来做自然语言问数。
|
|
45
|
+
|
|
46
|
+
- **零运行时依赖**:只用 Node 内置的 `fetch`,装进 profile 后不需要任何 `node_modules`。
|
|
47
|
+
- **只读**:客户端 SQL 护栏 + 可选的服务端 `readonly=1`,双层拦住写操作。
|
|
48
|
+
- **为数据问答设计**:7 个工具按「先摸清结构、再问数」的顺序编排,并支持按字段名反查表。
|
|
49
|
+
|
|
50
|
+
## 安装(写进 profile)
|
|
51
|
+
|
|
52
|
+
包本身是一个 **bundle**:自带 `cordis.patch.yml`,内容是一行 `insert: [{ id: clickhouse, name: @saichao2026/dsh-clickhouse, config: … }]`。
|
|
53
|
+
|
|
54
|
+
一条命令(幂等,可反复跑):
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
pnpm install
|
|
58
|
+
pnpm build
|
|
59
|
+
node scripts/install.mjs
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`install.mjs` 做四件事:
|
|
63
|
+
|
|
64
|
+
1. 把 `package.json` / `README.md` / `lib` 复制到 `<profile>/node_modules/@saichao2026/dsh-clickhouse` 与 `~/.dsh/node_modules/@saichao2026/dsh-clickhouse`(后者是 profile 的 `node_modules` 被 pnpm 清理时的兜底解析路径);
|
|
65
|
+
2. 把部署配置写进两份拷贝的 `cordis.patch.yml`(源仓库那份保持通用、不带 config);
|
|
66
|
+
3. 确保 `@saichao2026/dsh-clickhouse` 在 profile `package.json` 的 `dsh.profile.bundles` 里(**改动前备份到 `backups/`**,临时文件 + rename 原子写);
|
|
67
|
+
4. 跑 `scripts/verify.mjs` 预检。
|
|
68
|
+
|
|
69
|
+
然后重启 DSH。补丁文件是外部改的,宿主不会热加载它(实测桌面端改完不重启,插件仍按旧配置工作),配置改动一律重启后确认。
|
|
70
|
+
|
|
71
|
+
**回滚**:`node scripts/uninstall.mjs`(从 `dsh.profile.bundles` 摘掉 + 删两份拷贝;删目录前会核对 `package.json.name`,`--dry-run` 只看不做)。
|
|
72
|
+
|
|
73
|
+
### 手工安装(不想跑脚本时)
|
|
74
|
+
|
|
75
|
+
```powershell
|
|
76
|
+
$profile = "$env:USERPROFILE\.dsh\profiles\desktop"
|
|
77
|
+
New-Item -ItemType Directory -Force -Path "$profile\node_modules\@saichao2026/dsh-clickhouse" | Out-Null
|
|
78
|
+
Copy-Item package.json, cordis.patch.yml, README.md, lib -Recurse -Force "$profile\node_modules\@saichao2026/dsh-clickhouse"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
再把 `"@saichao2026/dsh-clickhouse"` 追加到 `$profile\package.json` 的 `dsh.profile.bundles`(**先备份这个文件**)。
|
|
82
|
+
|
|
83
|
+
## 配置
|
|
84
|
+
|
|
85
|
+
连接信息**默认已经内置**(见「两步上手」),这一节只讲**要换服务器时**怎么覆盖。覆盖写在 **profile 的用户补丁层**(`~/.dsh/profiles/<name>/cordis.patch.yml`),按 id 替换 bundle 里那一行:
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
- id: clickhouse
|
|
89
|
+
name: @saichao2026/dsh-clickhouse
|
|
90
|
+
config:
|
|
91
|
+
url: http://<主机>:8123
|
|
92
|
+
user: default
|
|
93
|
+
password: ''
|
|
94
|
+
database: default
|
|
95
|
+
readonly: true
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
两处配置的关系:`install.mjs` 会把同样一份写进**已安装拷贝**的 `cordis.patch.yml`(裸装即可用);用户补丁层在组装顺序里靠后,按 id **整份替换**该行的 `config`(Loader 的语义就是 `target[key] = value`)。写进用户补丁层还有个好处:**DSH 的 HMR 会立即重组整棵树,不用重启**——实测写完覆盖行后,对话里的工具立刻连到新地址。
|
|
99
|
+
|
|
100
|
+
> 改这个文件前务必备份:它同时存放着模型/密钥等配置,写坏会让 DSH 启动失败并触发桌面端的恢复流程(把该文件改名藏起来)。
|
|
101
|
+
|
|
102
|
+
| 配置项 | 默认值 | 说明 |
|
|
103
|
+
| --- | --- | --- |
|
|
104
|
+
| `url` | `http://10.0.0.34:8123` | ClickHouse **HTTP** 接口地址,内置默认值就是部署环境(装完免配)。也接受 JDBC 写法:`jdbc:clickhouse://host:8123/db?x=y`(自动转成 `http://host:8123`,路径段当作默认库)。 |
|
|
105
|
+
| `user` / `password` | `default` / 空 | 也兼容 `CLICKHOUSE_USER` / `CLICKHOUSE_PASSWORD` 这类环境变量命名,方便从 MCP 配置直接搬。 |
|
|
106
|
+
| `database` | `default` | 未指定库时的默认库。 |
|
|
107
|
+
| `readonly` | `true` | 为 true 时给每个查询加服务端 `readonly=1`。老版本(如 21.12)上 `numbers()` 等表函数在只读模式下会被拒;确需时设为 `false`(客户端护栏仍然生效)。 |
|
|
108
|
+
| `maxRows` | `200` | 单次返回给模型的最大行数(硬上限 10000)。 |
|
|
109
|
+
| `timeoutSeconds` | `60` | 服务端 `max_execution_time`(硬上限 600)。 |
|
|
110
|
+
| `maxMemoryGb` | `4` | 服务端 `max_memory_usage`。 |
|
|
111
|
+
| `maxCellChars` | `400` | 单个单元格保留的最大字符数。 |
|
|
112
|
+
| `allowQuerySettings` | `false` | 是否允许 SQL 自带 `SETTINGS ...`。 |
|
|
113
|
+
| `allowExternalSources` | `false` | 是否允许 `file()`/`url()`/`s3()` 等外部数据源表函数。 |
|
|
114
|
+
| `allowCrossTable` | `false` | 是否允许 JOIN / UNION / 一次引用多张表。默认 `false`:本插件按**单表查询**使用——用户给什么参数就用什么参数,跨表的业务判断不写进插件。 |
|
|
115
|
+
| `allowDatabases` | `[]` | 只允许访问这些库(空 = 不限制)。 |
|
|
116
|
+
| `denyDatabases` | `[]` | 禁止访问的库(优先级高于白名单)。 |
|
|
117
|
+
| `sampleRows` | `20` | `clickhouse_sample_rows` 的默认抽样行数。 |
|
|
118
|
+
| `guidance` | `true` | 是否往系统提示里注入一段本插件的使用说明。 |
|
|
119
|
+
|
|
120
|
+
## 工具
|
|
121
|
+
|
|
122
|
+
| 工具 | 用途 |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `clickhouse_query` | 执行单条只读 SQL(`SELECT`/`WITH`/`SHOW`/`DESCRIBE`/`EXPLAIN`/`EXISTS`),返回 Markdown 表格或 JSON。 |
|
|
125
|
+
| `clickhouse_list_databases` | 列出库 + 每库表数/字段数(可选磁盘占用)。 |
|
|
126
|
+
| `clickhouse_list_tables` | 列出某库的表:引擎、排序键、行数、占用、注释,按占用倒序。 |
|
|
127
|
+
| `clickhouse_describe_table` | 表的字段、类型、默认值、主键/排序键成员、注释。 |
|
|
128
|
+
| `clickhouse_sample_rows` | 抽样看真实数据,可只取指定字段。 |
|
|
129
|
+
| `clickhouse_explain` | `EXPLAIN` 执行计划(plan / estimate / syntax / ast / pipeline)。 |
|
|
130
|
+
| `clickhouse_search_columns` | 按**字段名**关键词反查「这个字段在哪张表」,不确定数据在哪时用。 |
|
|
131
|
+
|
|
132
|
+
模型看到的每个工具返回值都是结构化的(`summary` / `columns` / `rows` / `rowCount` / `truncated` / `elapsedMs` / `queryId` / `executedSql` / `notes`),并由工具自己渲染成 Markdown 表格。
|
|
133
|
+
|
|
134
|
+
## 实际效果
|
|
135
|
+
|
|
136
|
+
模型在对话里看到的就是这样的内容(`docs/demo-output.txt` 是完整演示输出):
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
### clickhouse_query
|
|
140
|
+
SELECT database AS `库`, count() AS `表数` FROM system.tables
|
|
141
|
+
WHERE database LIKE 'system%' GROUP BY database ORDER BY `表数` DESC LIMIT 5
|
|
142
|
+
|
|
143
|
+
查询完成:1 行 / 2 ms
|
|
144
|
+
|
|
145
|
+
- rows: 1 | limit: 200 | elapsed: 2 ms
|
|
146
|
+
- query_id: dsh-clickhouse-query-mvqd5pyu-8-qcc351
|
|
147
|
+
- statistics: rows_read=75, bytes_read=1125, server_elapsed=0.001761516s
|
|
148
|
+
|
|
149
|
+
| 库 | 表数 |
|
|
150
|
+
| --- | --- |
|
|
151
|
+
| system | 75 |
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
(演示刻意只用任何实例都有的 `system` 库,方便你在自己机器上复现;`docs/demo-output.txt` 是完整输出。)
|
|
155
|
+
|
|
156
|
+
护栏不引入额外开销:SQL 直接下推给 ClickHouse,千万行级的 group by 是百毫秒级。中文别名能正常用(**必须加反引号**——老版本 ClickHouse 不接受未加引号的非 ASCII 标识符,插件在这种情况下会直接给出改法提示)。
|
|
157
|
+
|
|
158
|
+
> 踩过的坑(已在 `test/sql-guard.test.ts` 里加了回归测试):SQL 护栏早先用「把引号内容抹成空格」的掩码去找最后一个代码字符,导致 `SELECT 1 AS \`行数\`` 这类**以引号结尾**的语句被截断成 `SELECT 1 AS`。现在改成只抹注释、保留引号内容。
|
|
159
|
+
|
|
160
|
+
## 脚本
|
|
161
|
+
|
|
162
|
+
| 脚本 | 用途 |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `node scripts/install.mjs` | 构建产物安装/更新到 profile(幂等;保留已安装的 config;自动维护 `dsh.profile.bundles` 并备份;最后自动跑 `verify.mjs`)。`--config deploy.json` 可指定连接配置,`--no-home` 跳过兜底拷贝。 |
|
|
165
|
+
| `node scripts/uninstall.mjs` | 从 `dsh.profile.bundles` 摘掉它并删除拷贝(删目录前会核对 `package.json.name`)。`--dry-run` 只看不做。 |
|
|
166
|
+
| `node scripts/verify.mjs` | **重启前预检**:bundle 栈能否解析、插件包是否就位、loader 行是否声明、`apply()` 能否注册出 7 个工具。 |
|
|
167
|
+
| `node scripts/verify-deep.mjs` | 直接调用**市场自己的组装分析**(`dshmarket` 的 `analyzeProfile`),看层栈/重复 entry id/失效引用。 |
|
|
168
|
+
| `node scripts/demo.mjs` | 用已安装的那份配置真跑几个工具,打印模型可见的渲染结果。 |
|
|
169
|
+
| `scripts/inspect-composition.cjs` | 用 **loader 自己的代码**打印组装后的行(含每个插件的最终 `config`),等于「重启后插件实际拿到什么」的离线证据。需 Electron 的 `ELECTRON_RUN_AS_NODE` 模式跑。 |
|
|
170
|
+
|
|
171
|
+
连接信息写在**已安装拷贝**的 `cordis.patch.yml` 里(`install.mjs` 会保留),源仓库那份不带 config;正式的部署配置建议写进 profile 的用户补丁层。
|
|
172
|
+
|
|
173
|
+
## 验证(这台机器上的实测)
|
|
174
|
+
|
|
175
|
+
| 验证项 | 做法 | 结果 |
|
|
176
|
+
| --- | --- | --- |
|
|
177
|
+
| 离线测试 | `pnpm test` | 47/47 通过 |
|
|
178
|
+
| 真机联调 | `pnpm test:live`(连一个 ClickHouse 21.12 实例) | 14/14 通过 |
|
|
179
|
+
| 组装预检 | `node scripts/verify.mjs` | 通过(bundle 栈可解析、行已声明、7 个工具能注册) |
|
|
180
|
+
| 市场侧的组装分析 | `node scripts/verify-deep.mjs`、`GET /dsh-market/check` | `duplicates` / `orderConflicts` / `residuals` 全空,解析错误 0 |
|
|
181
|
+
| **config 是否真进了组装** | `scripts/inspect-composition.cjs`(调 loader 自己的 `loadProfile` + `composeEntries`) | 组装出的行里 `config.url` 就是部署时写进去的那个地址 |
|
|
182
|
+
| **活实例可用性** | 在对话里直接调 `clickhouse_*` 工具 | 能列库列表、能对千万行级大表 group by、中文别名提示到位 |
|
|
183
|
+
| 热生效(不重启) | 往用户补丁层写覆盖行 → DSH 的 HMR 重组 | 工具立刻连到新地址 |
|
|
184
|
+
|
|
185
|
+
`inspect-composition.cjs` 需要 asar 感知的 node(Loader 在 `app.asar` 内),所以用 Electron 的 node 模式跑:
|
|
186
|
+
|
|
187
|
+
```powershell
|
|
188
|
+
$env:ELECTRON_RUN_AS_NODE = '1'
|
|
189
|
+
& "$env:LOCALAPPDATA\Programs\DeepSeek Harness\DeepSeek Harness.exe" scripts\inspect-composition.cjs --row clickhouse
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
> 一个值得记住的坑:desktop host 是**长驻进程**,插件是热加载进去的。如果它在补丁文件写入 config **之前**就已经加载过这个插件,内存里的行会一直是旧配置(表现:工具去连 `localhost:8123`),而磁盘上的组装结果其实已经是对的。往用户补丁层写覆盖会触发 HMR 重组并立即生效,否则重启一次也会好。
|
|
193
|
+
|
|
194
|
+
## 只读护栏
|
|
195
|
+
|
|
196
|
+
`clickhouse_query` 与 `clickhouse_explain` 的 SQL 会先过一遍客户端护栏:
|
|
197
|
+
|
|
198
|
+
1. 只允许**单条**语句;
|
|
199
|
+
2. 首关键字必须是 `SELECT` / `WITH` / `SHOW` / `DESCRIBE` / `DESC` / `EXPLAIN` / `EXISTS`;
|
|
200
|
+
3. 任何位置出现 `INSERT`/`UPDATE`/`DELETE`/`ALTER`/`CREATE`/`DROP`/`TRUNCATE`/`RENAME`/`ATTACH`/`DETACH`/`OPTIMIZE`/`GRANT`/`REVOKE`/`KILL` 等写关键字即拒绝(同名标识符可用反引号包起来);
|
|
201
|
+
4. 拒绝 `INTO OUTFILE` / `INTO DUMPFILE`;
|
|
202
|
+
5. 拒绝 SQL 自带的 `FORMAT ...`(结果格式由插件控制)与 `SETTINGS ...`(除非 `allowQuerySettings`);
|
|
203
|
+
6. 默认拒绝 `file()`/`url()`/`remote()`/`s3()`/`jdbc()`/`mysql()` 等外部数据源表函数(除非 `allowExternalSources`);
|
|
204
|
+
7. 默认**只允许单表查询**:出现 `JOIN`、或引用了第二张不同的表(含跨表子查询、跨表 `UNION`)即拒绝(除非 `allowCrossTable`)。
|
|
205
|
+
|
|
206
|
+
第 7 条是按「SQL 引用了哪些表」判定的,**不是按 WHERE 条件个数**:
|
|
207
|
+
|
|
208
|
+
```sql
|
|
209
|
+
-- ✅ 放行:一张表,多个字段、多个条件
|
|
210
|
+
SELECT patent_no, title FROM open_api_patent.dim_patent_main
|
|
211
|
+
WHERE title LIKE '%地址%' AND holder_first LIKE '%畅远%'
|
|
212
|
+
OR abstr LIKE '%地址%';
|
|
213
|
+
|
|
214
|
+
-- ✅ 放行:同一张表的子查询、ARRAY JOIN、`t1, t2` 同表两次
|
|
215
|
+
SELECT patent_no FROM t WHERE patent_no IN (SELECT patent_no FROM t WHERE title LIKE '%x%');
|
|
216
|
+
|
|
217
|
+
-- ⛔ 拒绝:JOIN / 第二条不同的表 / 跨表子查询
|
|
218
|
+
SELECT p.title FROM open_api_patent.dim_patent_main AS p
|
|
219
|
+
JOIN open_api_enterprise.db_enterprise_t_enterprise AS e ON p.patent_no = e.credit_no;
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
设计取舍:单表限制让「查什么表」完全由用户给的参数决定,插件不预设任何业务流程(比如「企业名先换信用代码再查别的表」)。代价是名称类字段没有索引,`LIKE '%参数%'` 会是全表扫——这是**透明 > 聪明**的有意选择;确实需要跨表时把 `allowCrossTable` 设为 `true`。
|
|
223
|
+
|
|
224
|
+
另外每个查询都会带上服务端上限:`max_result_rows`(配合 `result_overflow_mode=break` 直接截断)、`max_execution_time`、`max_memory_usage`。
|
|
225
|
+
|
|
226
|
+
> 护栏是「防手滑」,不是权限边界。真正的权限请用 ClickHouse 账号本身控制(建一个只有 `SELECT` 权限的账号给这个插件用)。
|
|
227
|
+
|
|
228
|
+
## 开发
|
|
229
|
+
|
|
230
|
+
```powershell
|
|
231
|
+
pnpm install
|
|
232
|
+
pnpm typecheck # 类型检查
|
|
233
|
+
pnpm test # 离线测试(52 项:护栏、格式化、插件契约、参数校验)
|
|
234
|
+
pnpm test:live # 真机联调(14 项,默认连 http://localhost:8123 的 system.tables)
|
|
235
|
+
pnpm test:all
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
真机测试用环境变量覆盖连接信息:`CLICKHOUSE_TEST_URL` / `CLICKHOUSE_TEST_USER` / `CLICKHOUSE_TEST_PASSWORD` / `CLICKHOUSE_TEST_DATABASE` / `CLICKHOUSE_TEST_TABLE`。
|
|
239
|
+
|
|
240
|
+
## 为什么不依赖 `@deepseek-ai/dsh-tools`
|
|
241
|
+
|
|
242
|
+
`dsh-tools` 的 peer 依赖图里有未发布到 npm 的内部包(`@deepseek-ai/dsh-type-meta`),从 registry 装不上;而 profile 插件运行时也不需要它——工具定义是纯数据结构。所以本插件按其官方文档支持的**原始注册**方式注册工具,自己负责参数校验与输出声明,代价是几十行校验代码,收益是插件在 profile 里零依赖、加载路径上没有依赖解析风险。参数与输出都用标准 JSON Schema(`output.schema` 会被注册表强制执行)。
|
|
243
|
+
|
|
244
|
+
## 不支持 / 边界
|
|
245
|
+
|
|
246
|
+
- 护栏是「防手滑」,不是权限边界(见文末)。
|
|
247
|
+
- 单次最多 200 行(可配,硬上限 10000);要更多请自己聚合,别拉明细。
|
|
248
|
+
- 结果以 `JSONCompact` 取回后在内存里渲染,超大结果不适合。
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT © 2026 赛超,见 [LICENSE](./LICENSE)。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClickHouse HTTP 接口客户端(零依赖,只用全局 fetch)。
|
|
3
|
+
*
|
|
4
|
+
* 选型说明:不用 `@clickhouse/client`。插件装进 DSH profile 后应当**零依赖**,
|
|
5
|
+
* 否则每次启动都要解析一遍 node_modules;而 HTTP 接口本身足够简单——
|
|
6
|
+
* POST SQL 到 `http://host:8123/?<settings>`,用 `default_format=JSON` 拿结构化结果。
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-clickhouse/clickhouse
|
|
9
|
+
*/
|
|
10
|
+
import { UserError } from "./errors.js";
|
|
11
|
+
import { type ClickHousePluginConfig } from "./config.js";
|
|
12
|
+
/** 服务端返回的查询统计。 */
|
|
13
|
+
export interface QueryStatistics {
|
|
14
|
+
rows_read?: number;
|
|
15
|
+
bytes_read?: number;
|
|
16
|
+
elapsed?: number;
|
|
17
|
+
}
|
|
18
|
+
/** 一次查询的完整结果(字段与 `format.ts` 的渲染器对齐)。 */
|
|
19
|
+
export interface QueryOutcome {
|
|
20
|
+
columns: string[];
|
|
21
|
+
columnTypes: string[];
|
|
22
|
+
rows: Record<string, unknown>[];
|
|
23
|
+
truncated: boolean;
|
|
24
|
+
limit: number;
|
|
25
|
+
elapsedMs: number;
|
|
26
|
+
queryId: string;
|
|
27
|
+
statistics?: QueryStatistics;
|
|
28
|
+
rowsBeforeLimitAtLeast?: number;
|
|
29
|
+
executedSql: string;
|
|
30
|
+
sqlWasRewritten: boolean;
|
|
31
|
+
notes: string[];
|
|
32
|
+
}
|
|
33
|
+
export interface ExecuteOptions {
|
|
34
|
+
/** 单次返回的最大行数(服务端按 limit+1 取,多出的那一行仅用来判断是否被截断)。 */
|
|
35
|
+
limit: number;
|
|
36
|
+
signal?: AbortSignal | undefined;
|
|
37
|
+
/** 调用用途,仅用于 query_id / log_comment,便于在 system.query_log 里追溯。 */
|
|
38
|
+
label?: string;
|
|
39
|
+
}
|
|
40
|
+
/** ClickHouse 服务端返回的错误(带错误码),消息已整理成可直接展示的形式。 */
|
|
41
|
+
export declare class ClickHouseQueryError extends UserError {
|
|
42
|
+
readonly code: number;
|
|
43
|
+
readonly errorType: string;
|
|
44
|
+
/** 该错误是否由“服务端只读 + 本插件的设置”引起。 */
|
|
45
|
+
readonly readonlyHint: boolean;
|
|
46
|
+
readonly name: string;
|
|
47
|
+
constructor(code: number, errorType: string, detail: string,
|
|
48
|
+
/** 该错误是否由“服务端只读 + 本插件的设置”引起。 */
|
|
49
|
+
readonlyHint?: boolean);
|
|
50
|
+
}
|
|
51
|
+
/** 把服务端的纯文本错误体解析成带错误码的异常。 */
|
|
52
|
+
export declare function parseServerError(text: string): ClickHouseQueryError | null;
|
|
53
|
+
/**
|
|
54
|
+
* 连接层失败的友好提示。
|
|
55
|
+
*
|
|
56
|
+
* `text` 已经展开过 `error.cause`(见 `describeThrown`),所以这里能认出 undici 藏在
|
|
57
|
+
* cause 里的 ECONNREFUSED / ENOTFOUND / 超时,而不是一律落到最后那句 "fetch failed"。
|
|
58
|
+
* 用着默认地址时再补一段「去哪儿写配置」——新用户就是卡在这里。
|
|
59
|
+
*
|
|
60
|
+
* 导出仅为便于测试。
|
|
61
|
+
*/
|
|
62
|
+
export declare function describeNetworkError(error: unknown, config: ClickHousePluginConfig): UserError;
|
|
63
|
+
/** 把库级/库名非法等错误统一成可读提示。 */
|
|
64
|
+
export declare function assertDatabaseAllowed(config: ClickHousePluginConfig, database: string): void;
|
|
65
|
+
/** 生成 ClickHouse 的反引号标识符,同时挡住注入。 */
|
|
66
|
+
export declare function quoteIdentifier(name: string): string;
|
|
67
|
+
/** 把 `db.table` / `table` 解析成 `{ database, table }`。 */
|
|
68
|
+
export declare function resolveTableName(table: string, database: string | undefined, fallbackDatabase: string): {
|
|
69
|
+
database: string;
|
|
70
|
+
table: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* 单次查询的 HTTP 客户端。
|
|
74
|
+
*
|
|
75
|
+
* 每个查询都会带上服务端护栏:`max_result_rows` + `result_overflow_mode=break`
|
|
76
|
+
* (超限直接截断而不是报错)、`max_execution_time`、`max_memory_usage`,以及配置允许时的
|
|
77
|
+
* `readonly=1`。
|
|
78
|
+
*/
|
|
79
|
+
export declare class ClickHouseGateway {
|
|
80
|
+
private readonly config;
|
|
81
|
+
constructor(config: ClickHousePluginConfig);
|
|
82
|
+
/** 组装 URL 参数(即 ClickHouse 的查询设置)。 */
|
|
83
|
+
private buildParams;
|
|
84
|
+
/** 真正发一次 HTTP 请求,返回原始文本。 */
|
|
85
|
+
private post;
|
|
86
|
+
/** 把 JSON 信封转成 QueryOutcome。 */
|
|
87
|
+
private materialize;
|
|
88
|
+
/**
|
|
89
|
+
* 执行一条内部构造的 SQL(不含护栏;调用方负责只读)。
|
|
90
|
+
*
|
|
91
|
+
* 若服务端因只读模式拒绝了本插件附加的设置,会去掉这些设置重试一次——护栏仍在
|
|
92
|
+
* 客户端生效。
|
|
93
|
+
*/
|
|
94
|
+
private run;
|
|
95
|
+
/** 执行模型给的 SQL:先过只读护栏,再加行数上限。 */
|
|
96
|
+
executeUserQuery(sql: string, options: {
|
|
97
|
+
signal?: AbortSignal | undefined;
|
|
98
|
+
maxRows?: number | undefined;
|
|
99
|
+
}): Promise<QueryOutcome>;
|
|
100
|
+
/** 执行插件自己拼的元数据查询(如 system.tables)。 */
|
|
101
|
+
executeMetadataQuery(sql: string, options?: {
|
|
102
|
+
signal?: AbortSignal | undefined;
|
|
103
|
+
limit?: number | undefined;
|
|
104
|
+
label?: string | undefined;
|
|
105
|
+
}): Promise<QueryOutcome>;
|
|
106
|
+
/** 连通性 + 版本信息。 */
|
|
107
|
+
serverInfo(signal?: AbortSignal): Promise<{
|
|
108
|
+
version: string;
|
|
109
|
+
timezone: string;
|
|
110
|
+
uptimeSeconds: number;
|
|
111
|
+
}>;
|
|
112
|
+
}
|