dsh-sql 0.4.1 → 0.4.2
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/README.en.md +12 -4
- package/README.md +12 -4
- package/lib/adapters.d.ts +6 -6
- package/lib/adapters.js +202 -61
- package/lib/index.d.ts +5 -4
- package/lib/index.js +5 -11
- package/lib/tools.d.ts +1 -1
- package/lib/tools.js +37 -25
- package/package.json +2 -2
package/README.en.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
> **Your agent can query databases now**: SQLite / MySQL / PostgreSQL engines, read-only whitelist + write approval gate.
|
|
6
6
|
|
|
7
|
-
DSH (DeepSeek Harness) engineer-grade database plugin:
|
|
7
|
+
DSH (DeepSeek Harness) engineer-grade database plugin: six tools covering connection management, read-only queries, write operations, schema introspection, database statistics, and health checks.
|
|
8
8
|
|
|
9
9
|
   
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ DSH (DeepSeek Harness) engineer-grade database plugin: four tools covering conne
|
|
|
12
12
|
|
|
13
13
|
## Compatibility
|
|
14
14
|
|
|
15
|
-
Verified against
|
|
15
|
+
Verified against the official `@deepseek-ai/dsh@0.1.2-rc.1` release and current source checkout on 2026-09-04. `0.1.2-alpha.5` remains supported because both releases use the same plugin/tool contract. Built for the cordis patch-bundle model (`cordis.patch.yml` + `dsh.bundle.patch`) with no runtime imports of `@deepseek-ai/*` internals.
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
@@ -57,6 +57,8 @@ Then restart the web service. To clean up fully, also remove the plugin entry fr
|
|
|
57
57
|
writeApproval: true # approve write operations first (default true)
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
With no connection configuration, the plugin provides a `:memory:` SQLite connection. If configuration is present but invalid, the plugin fails to load with the validation error instead of silently falling back to the in-memory database.
|
|
61
|
+
|
|
60
62
|
## Tools
|
|
61
63
|
|
|
62
64
|
| Tool | Purpose | Safety |
|
|
@@ -65,6 +67,8 @@ Then restart the web service. To clean up fully, also remove the plugin entry fr
|
|
|
65
67
|
| `sql_query` | Read-only queries (SELECT/PRAGMA/EXPLAIN/SHOW/DESCRIBE/WITH) | Keyword whitelist + rejects multi-statement |
|
|
66
68
|
| `sql_exec` | Writes / DDL (multi-statement scripts allowed) | readOnly lock + approval gate |
|
|
67
69
|
| `sql_schema` | Table list / table structure | Identifier whitelist validation |
|
|
70
|
+
| `sql_stats` | Table counts, row estimates, and database size | Quoted identifiers + isolated query failures |
|
|
71
|
+
| `sql_health` | Connection and safety-configuration checks | Per-connection probe; passwords are never returned |
|
|
68
72
|
|
|
69
73
|
### Examples
|
|
70
74
|
|
|
@@ -72,6 +76,8 @@ Then restart the web service. To clean up fully, also remove the plugin entry fr
|
|
|
72
76
|
sql_list {}
|
|
73
77
|
sql_schema {} # list all tables
|
|
74
78
|
sql_schema { table: users } # inspect the users table
|
|
79
|
+
sql_stats {} # inspect the default connection's data size
|
|
80
|
+
sql_health {} # check connections and safety settings
|
|
75
81
|
sql_query { sql: SELECT * FROM orders WHERE status = 'pending' LIMIT 50 }
|
|
76
82
|
sql_exec { sql: UPDATE orders SET status = 'paid' WHERE id = 42 }
|
|
77
83
|
```
|
|
@@ -81,7 +87,9 @@ sql_exec { sql: UPDATE orders SET status = 'paid' WHERE id = 42 }
|
|
|
81
87
|
- **Lexer-grade read-only guard**: sql_query strips strings/comments before validation, then rejects data-modifying CTEs (WITH…DELETE/UPDATE), SELECT INTO, FOR UPDATE/FOR SHARE, PRAGMA assignment, and multi-statement input
|
|
82
88
|
- **Write approval gate**: sql_exec asks for approval by default (mirroring dsh-email's send approval); headless environments without an approval channel are denied
|
|
83
89
|
- **readOnly mode**: lock out writes entirely for production databases
|
|
84
|
-
- **Streaming row cap**: SQLite
|
|
90
|
+
- **Streaming row cap**: SQLite iterators, MySQL Readables, and PostgreSQL Query row events collect at most maxRows+1 rows and flag overflow with truncated. MySQL and PostgreSQL close the query's dedicated connection at the cap; smaller results return the connection to the pool, without materializing the full result in memory
|
|
91
|
+
- **Cancellation-aware execution**: queries and writes observe Harness `exec.signal`; cancellation stops waiting and destroys the active dedicated MySQL/PostgreSQL connection
|
|
92
|
+
- **Lossless large integers**: bigint values within JavaScript's safe integer range are returned as numbers; larger values are returned as decimal strings instead of silently losing precision
|
|
85
93
|
- **Identifier validation**: table names restricted to alphanumerics and underscores — no schema injection
|
|
86
94
|
- **Secrets stay out of config**: passwords via `DSH_SQL_PASSWORD_<CONNECTION>` env vars
|
|
87
95
|
|
|
@@ -95,7 +103,7 @@ sql_exec { sql: UPDATE orders SET status = 'paid' WHERE id = 42 }
|
|
|
95
103
|
|
|
96
104
|
```bash
|
|
97
105
|
pnpm install
|
|
98
|
-
pnpm test # build +
|
|
106
|
+
pnpm test # build + full test suite, including a real SQLite integration suite
|
|
99
107
|
```
|
|
100
108
|
|
|
101
109
|
## License
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
> **你的 agent 会查库了**:SQLite / MySQL / PostgreSQL 三引擎,只读白名单 + 写审批门。
|
|
6
6
|
|
|
7
|
-
DSH(DeepSeek Harness
|
|
7
|
+
DSH(DeepSeek Harness)工程师级数据库插件:六个工具覆盖连接管理、只读查询、写操作、结构探查、统计概览与健康自检。
|
|
8
8
|
|
|
9
9
|
   
|
|
10
10
|
|
|
@@ -12,7 +12,7 @@ DSH(DeepSeek Harness)工程师级数据库插件:四个工具覆盖连接
|
|
|
12
12
|
|
|
13
13
|
## 兼容性
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
已在官方 `@deepseek-ai/dsh@0.1.2-rc.1` 与最新源码模式下验证(2026-09-04);`0.1.2-alpha.5` 仍可使用,两版插件/工具契约一致。遵循 cordis 组合包补丁模型(`cordis.patch.yml` + `dsh.bundle.patch`),运行时不 import 任何 `@deepseek-ai/*` 内部模块。
|
|
16
16
|
|
|
17
17
|
## 安装
|
|
18
18
|
|
|
@@ -57,6 +57,8 @@ dsh plugin --profile web remove dsh-sql
|
|
|
57
57
|
writeApproval: true # 写操作先弹审批(默认 true)
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
配置缺省时会提供一个 `:memory:` SQLite 连接;配置一旦给出但格式无效,插件会直接报错并停止加载,不会静默回退到内存库。
|
|
61
|
+
|
|
60
62
|
## 工具一览
|
|
61
63
|
|
|
62
64
|
| 工具 | 作用 | 安全 |
|
|
@@ -65,6 +67,8 @@ dsh plugin --profile web remove dsh-sql
|
|
|
65
67
|
| `sql_query` | 只读查询(SELECT/PRAGMA/EXPLAIN/SHOW/DESCRIBE/WITH)| 关键字白名单 + 拒绝多语句 |
|
|
66
68
|
| `sql_exec` | 写操作/DDL(可多语句脚本)| readOnly 禁用 + 审批门 |
|
|
67
69
|
| `sql_schema` | 表清单 / 表结构 | 标识符白名单校验 |
|
|
70
|
+
| `sql_stats` | 表数量、行数与库体积概览 | 表名引用 + 查询失败隔离 |
|
|
71
|
+
| `sql_health` | 连接与安全配置自检 | 逐连接探活,不回显密码 |
|
|
68
72
|
|
|
69
73
|
### 示例
|
|
70
74
|
|
|
@@ -72,6 +76,8 @@ dsh plugin --profile web remove dsh-sql
|
|
|
72
76
|
sql_list {}
|
|
73
77
|
sql_schema {} # 列出所有表
|
|
74
78
|
sql_schema { table: users } # 看 users 表结构
|
|
79
|
+
sql_stats {} # 查看默认连接的数据规模
|
|
80
|
+
sql_health {} # 检查连接和安全配置
|
|
75
81
|
sql_query { sql: SELECT * FROM orders WHERE status = 'pending' LIMIT 50 }
|
|
76
82
|
sql_exec { sql: UPDATE orders SET status = 'paid' WHERE id = 42 }
|
|
77
83
|
```
|
|
@@ -81,7 +87,9 @@ sql_exec { sql: UPDATE orders SET status = 'paid' WHERE id = 42 }
|
|
|
81
87
|
- **词法级只读保护**:sql_query 先剥离字符串/注释再校验,拒绝 data-modifying CTE(WITH…DELETE/UPDATE)、SELECT INTO、FOR UPDATE/FOR SHARE、PRAGMA 赋值与多语句
|
|
82
88
|
- **写审批门**:sql_exec 默认弹审批(对齐 dsh-email 的发信审批),headless 环境无审批通道时拒绝执行
|
|
83
89
|
- **readOnly 模式**:生产库可整体禁用写
|
|
84
|
-
- **流式行数钳制**:SQLite 迭代器 / MySQL
|
|
90
|
+
- **流式行数钳制**:SQLite 迭代器 / MySQL Readable / PostgreSQL Query 行事件最多收集 maxRows+1 行,超量标记 truncated;MySQL 和 PostgreSQL 在达到上限时关闭该查询的专用连接,未达上限则正常归还连接池,避免全量结果驻留内存
|
|
91
|
+
- **可取消执行**:查询与写操作遵守 Harness 的 `exec.signal`;取消时会中止等待并销毁正在工作的 MySQL/PostgreSQL 专用连接
|
|
92
|
+
- **大整数无损**:数据库返回的 bigint 在 JavaScript 安全整数范围内输出 number,超出范围则输出十进制字符串,避免静默丢精度
|
|
85
93
|
- **标识符校验**:表名只允许字母/数字/下划线,杜绝 schema 注入
|
|
86
94
|
- **密钥不落配置**:密码支持 `DSH_SQL_PASSWORD_<连接名>` 环境变量
|
|
87
95
|
|
|
@@ -95,7 +103,7 @@ sql_exec { sql: UPDATE orders SET status = 'paid' WHERE id = 42 }
|
|
|
95
103
|
|
|
96
104
|
```bash
|
|
97
105
|
pnpm install
|
|
98
|
-
pnpm test # 构建 +
|
|
106
|
+
pnpm test # 构建 + 完整测试套件(含真实 SQLite 集成)
|
|
99
107
|
```
|
|
100
108
|
|
|
101
109
|
## License
|
package/lib/adapters.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type SqlConnectionConfig } from './config.js';
|
|
2
|
-
/** 查询结果:列名 +
|
|
2
|
+
/** 查询结果:列名 + 行(值数组,无损 JSON 友好)。 */
|
|
3
3
|
export interface QueryResult {
|
|
4
4
|
columns: string[];
|
|
5
5
|
rows: unknown[][];
|
|
@@ -14,11 +14,11 @@ export interface ColumnInfo {
|
|
|
14
14
|
/** 统一适配器接口。 */
|
|
15
15
|
export interface DatabaseAdapter {
|
|
16
16
|
engine: 'sqlite' | 'mysql' | 'postgres';
|
|
17
|
-
listTables(): Promise<string[]>;
|
|
18
|
-
describeTable(table: string): Promise<ColumnInfo[]>;
|
|
19
|
-
query(sql: string, limit?: number): Promise<QueryResult>;
|
|
20
|
-
exec(sql: string): Promise<number>;
|
|
21
|
-
ping(): Promise<void>;
|
|
17
|
+
listTables(signal?: AbortSignal): Promise<string[]>;
|
|
18
|
+
describeTable(table: string, signal?: AbortSignal): Promise<ColumnInfo[]>;
|
|
19
|
+
query(sql: string, limit?: number, signal?: AbortSignal): Promise<QueryResult>;
|
|
20
|
+
exec(sql: string, signal?: AbortSignal): Promise<number>;
|
|
21
|
+
ping(signal?: AbortSignal): Promise<void>;
|
|
22
22
|
close(): Promise<void>;
|
|
23
23
|
}
|
|
24
24
|
/** 按连接配置创建适配器。 */
|
package/lib/adapters.js
CHANGED
|
@@ -8,9 +8,20 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
8
8
|
import mysql from 'mysql2/promise';
|
|
9
9
|
import pg from 'pg';
|
|
10
10
|
import { assertIdentifier } from './config.js';
|
|
11
|
+
function abortReason(signal) {
|
|
12
|
+
if (signal.reason !== undefined)
|
|
13
|
+
return signal.reason;
|
|
14
|
+
const error = new Error('The operation was aborted.');
|
|
15
|
+
error.name = 'AbortError';
|
|
16
|
+
return error;
|
|
17
|
+
}
|
|
11
18
|
function toValue(value) {
|
|
12
|
-
if (typeof value === 'bigint')
|
|
13
|
-
|
|
19
|
+
if (typeof value === 'bigint') {
|
|
20
|
+
if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
21
|
+
return Number(value);
|
|
22
|
+
}
|
|
23
|
+
return value.toString();
|
|
24
|
+
}
|
|
14
25
|
if (value instanceof Date)
|
|
15
26
|
return value.toISOString();
|
|
16
27
|
if (value instanceof Uint8Array)
|
|
@@ -27,6 +38,84 @@ function rowsToColumns(rows) {
|
|
|
27
38
|
function quoteSqliteIdentifier(name) {
|
|
28
39
|
return '"' + name.replace(/"/g, '""') + '"';
|
|
29
40
|
}
|
|
41
|
+
function streamMysqlQuery(corePool, sql, limit, discard) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
let settled = false;
|
|
44
|
+
let columns = [];
|
|
45
|
+
const rows = [];
|
|
46
|
+
const stream = corePool.query(sql).stream({ highWaterMark: 64 });
|
|
47
|
+
const finish = () => {
|
|
48
|
+
if (settled)
|
|
49
|
+
return;
|
|
50
|
+
settled = true;
|
|
51
|
+
resolve({ columns, rows });
|
|
52
|
+
};
|
|
53
|
+
stream.on('fields', (fields) => {
|
|
54
|
+
columns = fields.map((field) => field.name);
|
|
55
|
+
});
|
|
56
|
+
// Consume the Readable, otherwise mysql2 pauses forever at highWaterMark.
|
|
57
|
+
stream.on('data', (row) => {
|
|
58
|
+
if (settled)
|
|
59
|
+
return;
|
|
60
|
+
if (columns.length === 0)
|
|
61
|
+
columns = Object.keys(row);
|
|
62
|
+
rows.push(columns.map((name) => toValue(row[name])));
|
|
63
|
+
if (rows.length >= limit) {
|
|
64
|
+
finish();
|
|
65
|
+
stream.destroy();
|
|
66
|
+
// mysql2 resumes its connection when only the Readable is destroyed.
|
|
67
|
+
discard();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
stream.on('end', finish);
|
|
71
|
+
stream.on('close', finish);
|
|
72
|
+
stream.on('error', (error) => {
|
|
73
|
+
if (settled)
|
|
74
|
+
return;
|
|
75
|
+
settled = true;
|
|
76
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** pg's `rows` option is a page size; row events avoid its full result accumulator. */
|
|
81
|
+
function streamPostgresQuery(client, sql, limit, discard) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
let settled = false;
|
|
84
|
+
let columns = [];
|
|
85
|
+
const rows = [];
|
|
86
|
+
const query = new pg.Query(sql);
|
|
87
|
+
query.on('row', (row, result) => {
|
|
88
|
+
if (settled)
|
|
89
|
+
return;
|
|
90
|
+
if (columns.length === 0)
|
|
91
|
+
columns = result?.fields.map((field) => field.name) ?? Object.keys(row);
|
|
92
|
+
rows.push(columns.map((name) => toValue(row[name])));
|
|
93
|
+
if (rows.length >= limit) {
|
|
94
|
+
// Closing this dedicated connection stops server work and prevents reuse.
|
|
95
|
+
settled = true;
|
|
96
|
+
discard();
|
|
97
|
+
resolve({ columns, rows });
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
query.on('end', (result) => {
|
|
101
|
+
if (settled)
|
|
102
|
+
return;
|
|
103
|
+
settled = true;
|
|
104
|
+
if (columns.length === 0)
|
|
105
|
+
columns = result.fields.map((field) => field.name);
|
|
106
|
+
resolve({ columns, rows });
|
|
107
|
+
});
|
|
108
|
+
// Keep this listener after reaching the cap: destroying the client can emit
|
|
109
|
+
// the driver's asynchronous connection-closed error on this active query.
|
|
110
|
+
query.on('error', (error) => {
|
|
111
|
+
if (settled)
|
|
112
|
+
return;
|
|
113
|
+
settled = true;
|
|
114
|
+
reject(error);
|
|
115
|
+
});
|
|
116
|
+
client.query(query);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
30
119
|
/** SQLite 适配器(node:sqlite,零依赖)。 */
|
|
31
120
|
class SqliteAdapter {
|
|
32
121
|
engine = 'sqlite';
|
|
@@ -35,13 +124,17 @@ class SqliteAdapter {
|
|
|
35
124
|
this.db = new DatabaseSync(file === ':memory:' ? ':memory:' : file);
|
|
36
125
|
this.db.exec('PRAGMA busy_timeout = 5000');
|
|
37
126
|
}
|
|
38
|
-
async listTables() {
|
|
127
|
+
async listTables(signal) {
|
|
128
|
+
signal?.throwIfAborted();
|
|
39
129
|
const result = this.db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all();
|
|
130
|
+
signal?.throwIfAborted();
|
|
40
131
|
return result.map((row) => String(row.name));
|
|
41
132
|
}
|
|
42
|
-
async describeTable(table) {
|
|
133
|
+
async describeTable(table, signal) {
|
|
134
|
+
signal?.throwIfAborted();
|
|
43
135
|
const name = assertIdentifier(table, '表名');
|
|
44
136
|
const rows = this.db.prepare('PRAGMA table_info(' + quoteSqliteIdentifier(name) + ')').all();
|
|
137
|
+
signal?.throwIfAborted();
|
|
45
138
|
return rows.map((row) => ({
|
|
46
139
|
name: String(row.name),
|
|
47
140
|
type: String(row.type ?? ''),
|
|
@@ -49,10 +142,12 @@ class SqliteAdapter {
|
|
|
49
142
|
primaryKey: Number(row.pk) === 1,
|
|
50
143
|
}));
|
|
51
144
|
}
|
|
52
|
-
async query(sql, limit) {
|
|
145
|
+
async query(sql, limit, signal) {
|
|
146
|
+
signal?.throwIfAborted();
|
|
53
147
|
const statement = this.db.prepare(sql);
|
|
54
148
|
if (limit === undefined || limit <= 0) {
|
|
55
149
|
const rows = statement.all();
|
|
150
|
+
signal?.throwIfAborted();
|
|
56
151
|
return rowsToColumns(rows);
|
|
57
152
|
}
|
|
58
153
|
const columns = statement.columns().map((column) => column.name);
|
|
@@ -60,22 +155,28 @@ class SqliteAdapter {
|
|
|
60
155
|
for (const raw of statement.iterate()) {
|
|
61
156
|
const row = raw;
|
|
62
157
|
rows.push(columns.map((name) => toValue(row[name])));
|
|
158
|
+
signal?.throwIfAborted();
|
|
63
159
|
if (rows.length >= limit)
|
|
64
160
|
break;
|
|
65
161
|
}
|
|
66
162
|
return { columns, rows };
|
|
67
163
|
}
|
|
68
|
-
async exec(sql) {
|
|
164
|
+
async exec(sql, signal) {
|
|
165
|
+
signal?.throwIfAborted();
|
|
69
166
|
const single = sql.replace(/;\s*$/, '').trim();
|
|
70
167
|
if (single.includes(';')) {
|
|
71
168
|
this.db.exec(sql);
|
|
169
|
+
signal?.throwIfAborted();
|
|
72
170
|
return 0;
|
|
73
171
|
}
|
|
74
172
|
const result = this.db.prepare(single).run();
|
|
173
|
+
signal?.throwIfAborted();
|
|
75
174
|
return Number(result.changes);
|
|
76
175
|
}
|
|
77
|
-
async ping() {
|
|
176
|
+
async ping(signal) {
|
|
177
|
+
signal?.throwIfAborted();
|
|
78
178
|
this.db.prepare('SELECT 1').get();
|
|
179
|
+
signal?.throwIfAborted();
|
|
79
180
|
}
|
|
80
181
|
async close() {
|
|
81
182
|
this.db.close();
|
|
@@ -96,13 +197,45 @@ class MysqlAdapter {
|
|
|
96
197
|
enableKeepAlive: true,
|
|
97
198
|
});
|
|
98
199
|
}
|
|
99
|
-
async
|
|
100
|
-
|
|
200
|
+
async withSignalConnection(signal, work) {
|
|
201
|
+
signal?.throwIfAborted();
|
|
202
|
+
const connection = await this.pool.getConnection();
|
|
203
|
+
let destroyed = false;
|
|
204
|
+
let rejectAbort = () => { };
|
|
205
|
+
const aborted = new Promise((_resolve, reject) => { rejectAbort = reject; });
|
|
206
|
+
const discard = () => {
|
|
207
|
+
if (destroyed)
|
|
208
|
+
return;
|
|
209
|
+
destroyed = true;
|
|
210
|
+
connection.destroy();
|
|
211
|
+
};
|
|
212
|
+
const onAbort = () => {
|
|
213
|
+
discard();
|
|
214
|
+
rejectAbort(abortReason(signal));
|
|
215
|
+
};
|
|
216
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
217
|
+
try {
|
|
218
|
+
signal?.throwIfAborted();
|
|
219
|
+
return await Promise.race([work(connection, discard), aborted]);
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
signal?.removeEventListener('abort', onAbort);
|
|
223
|
+
if (!destroyed)
|
|
224
|
+
connection.release();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async queryRows(sql, signal) {
|
|
228
|
+
if (signal === undefined)
|
|
229
|
+
return await this.pool.query(sql);
|
|
230
|
+
return await this.withSignalConnection(signal, async (connection) => await connection.query(sql));
|
|
231
|
+
}
|
|
232
|
+
async listTables(signal) {
|
|
233
|
+
const [rows] = await this.queryRows('SHOW TABLES', signal);
|
|
101
234
|
return rows.map((row) => String(Object.values(row)[0] ?? ''));
|
|
102
235
|
}
|
|
103
|
-
async describeTable(table) {
|
|
236
|
+
async describeTable(table, signal) {
|
|
104
237
|
const name = assertIdentifier(table, '表名');
|
|
105
|
-
const [rows] = await this.
|
|
238
|
+
const [rows] = await this.queryRows('DESCRIBE `' + name + '`', signal);
|
|
106
239
|
return rows.map((row) => ({
|
|
107
240
|
name: String(row.Field),
|
|
108
241
|
type: String(row.Type ?? ''),
|
|
@@ -110,49 +243,22 @@ class MysqlAdapter {
|
|
|
110
243
|
primaryKey: String(row.Key ?? '').toUpperCase() === 'PRI',
|
|
111
244
|
}));
|
|
112
245
|
}
|
|
113
|
-
async query(sql, limit) {
|
|
246
|
+
async query(sql, limit, signal) {
|
|
114
247
|
if (limit === undefined || limit <= 0) {
|
|
115
|
-
const [rows] = await this.
|
|
248
|
+
const [rows] = await this.queryRows(sql, signal);
|
|
116
249
|
return rowsToColumns(rows);
|
|
117
250
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
let columns = [];
|
|
122
|
-
const rows = [];
|
|
123
|
-
const stream = corePool.query(sql).stream({ highWaterMark: 64 });
|
|
124
|
-
const finish = () => {
|
|
125
|
-
if (settled)
|
|
126
|
-
return;
|
|
127
|
-
settled = true;
|
|
128
|
-
resolve({ columns, rows });
|
|
129
|
-
};
|
|
130
|
-
stream.on('fields', (fields) => {
|
|
131
|
-
columns = fields.map((field) => field.name);
|
|
132
|
-
});
|
|
133
|
-
stream.on('result', (row) => {
|
|
134
|
-
if (columns.length === 0)
|
|
135
|
-
columns = Object.keys(row);
|
|
136
|
-
rows.push(columns.map((name) => toValue(row[name])));
|
|
137
|
-
if (rows.length >= limit)
|
|
138
|
-
stream.destroy();
|
|
139
|
-
});
|
|
140
|
-
stream.on('end', finish);
|
|
141
|
-
stream.on('close', finish);
|
|
142
|
-
stream.on('error', (error) => {
|
|
143
|
-
if (settled)
|
|
144
|
-
return;
|
|
145
|
-
settled = true;
|
|
146
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
147
|
-
});
|
|
251
|
+
return await this.withSignalConnection(signal, async (connection, discard) => {
|
|
252
|
+
const coreConnection = connection.connection;
|
|
253
|
+
return await streamMysqlQuery(coreConnection, sql, limit, discard);
|
|
148
254
|
});
|
|
149
255
|
}
|
|
150
|
-
async exec(sql) {
|
|
151
|
-
const [result] = await this.
|
|
256
|
+
async exec(sql, signal) {
|
|
257
|
+
const [result] = await this.queryRows(sql, signal);
|
|
152
258
|
return Number(result?.affectedRows ?? 0);
|
|
153
259
|
}
|
|
154
|
-
async ping() {
|
|
155
|
-
await this.
|
|
260
|
+
async ping(signal) {
|
|
261
|
+
await this.queryRows('SELECT 1', signal);
|
|
156
262
|
}
|
|
157
263
|
async close() {
|
|
158
264
|
await this.pool.end();
|
|
@@ -172,13 +278,48 @@ class PostgresAdapter {
|
|
|
172
278
|
max: 5,
|
|
173
279
|
});
|
|
174
280
|
}
|
|
175
|
-
async
|
|
176
|
-
|
|
281
|
+
async withSignalClient(signal, work) {
|
|
282
|
+
signal?.throwIfAborted();
|
|
283
|
+
const client = await this.pool.connect();
|
|
284
|
+
let destroyed = false;
|
|
285
|
+
let rejectAbort = () => { };
|
|
286
|
+
const aborted = new Promise((_resolve, reject) => { rejectAbort = reject; });
|
|
287
|
+
const discard = () => {
|
|
288
|
+
if (destroyed)
|
|
289
|
+
return;
|
|
290
|
+
destroyed = true;
|
|
291
|
+
client.release(true);
|
|
292
|
+
};
|
|
293
|
+
const onAbort = () => {
|
|
294
|
+
discard();
|
|
295
|
+
rejectAbort(abortReason(signal));
|
|
296
|
+
};
|
|
297
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
298
|
+
try {
|
|
299
|
+
signal?.throwIfAborted();
|
|
300
|
+
return await Promise.race([work(client, discard), aborted]);
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
signal?.removeEventListener('abort', onAbort);
|
|
304
|
+
if (!destroyed)
|
|
305
|
+
client.release();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async queryWithSignal(query, values, signal) {
|
|
309
|
+
const run = async (client) => {
|
|
310
|
+
return values === undefined ? await client.query(query) : await client.query(query, values);
|
|
311
|
+
};
|
|
312
|
+
if (signal === undefined)
|
|
313
|
+
return await run(this.pool);
|
|
314
|
+
return await this.withSignalClient(signal, run);
|
|
315
|
+
}
|
|
316
|
+
async listTables(signal) {
|
|
317
|
+
const result = await this.queryWithSignal("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name", undefined, signal);
|
|
177
318
|
return result.rows.map((row) => String(row.table_name));
|
|
178
319
|
}
|
|
179
|
-
async describeTable(table) {
|
|
320
|
+
async describeTable(table, signal) {
|
|
180
321
|
const name = assertIdentifier(table, '表名');
|
|
181
|
-
const result = await this.
|
|
322
|
+
const result = await this.queryWithSignal(`SELECT c.column_name, c.data_type, c.is_nullable,
|
|
182
323
|
EXISTS (
|
|
183
324
|
SELECT 1
|
|
184
325
|
FROM information_schema.table_constraints tc
|
|
@@ -193,7 +334,7 @@ class PostgresAdapter {
|
|
|
193
334
|
FROM information_schema.columns c
|
|
194
335
|
WHERE c.table_schema = 'public'
|
|
195
336
|
AND c.table_name = $1
|
|
196
|
-
ORDER BY c.ordinal_position`, [name]);
|
|
337
|
+
ORDER BY c.ordinal_position`, [name], signal);
|
|
197
338
|
return result.rows.map((row) => ({
|
|
198
339
|
name: String(row.column_name),
|
|
199
340
|
type: String(row.data_type ?? ''),
|
|
@@ -201,22 +342,22 @@ class PostgresAdapter {
|
|
|
201
342
|
primaryKey: row.is_primary === true,
|
|
202
343
|
}));
|
|
203
344
|
}
|
|
204
|
-
async query(sql, limit) {
|
|
345
|
+
async query(sql, limit, signal) {
|
|
205
346
|
if (limit === undefined || limit <= 0) {
|
|
206
|
-
const result = await this.
|
|
347
|
+
const result = await this.queryWithSignal(sql, undefined, signal);
|
|
207
348
|
const rows = result.rows;
|
|
208
349
|
return rowsToColumns(rows);
|
|
209
350
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
351
|
+
return await this.withSignalClient(signal, async (client, discard) => {
|
|
352
|
+
return await streamPostgresQuery(client, sql, limit, discard);
|
|
353
|
+
});
|
|
213
354
|
}
|
|
214
|
-
async exec(sql) {
|
|
215
|
-
const result = await this.
|
|
355
|
+
async exec(sql, signal) {
|
|
356
|
+
const result = await this.queryWithSignal(sql, undefined, signal);
|
|
216
357
|
return Number(result.rowCount ?? 0);
|
|
217
358
|
}
|
|
218
|
-
async ping() {
|
|
219
|
-
await this.
|
|
359
|
+
async ping(signal) {
|
|
360
|
+
await this.queryWithSignal('SELECT 1', undefined, signal);
|
|
220
361
|
}
|
|
221
362
|
async close() {
|
|
222
363
|
await this.pool.end();
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-sql —— 工程师级数据库工具插件(node 半身,配置走 cordis.patch.yml)。
|
|
3
3
|
*
|
|
4
|
-
* 插件导出 apply(ctx, config)
|
|
5
|
-
* sql_exec / sql_schema),支持 SQLite / MySQL / PostgreSQL
|
|
4
|
+
* 插件导出 apply(ctx, config):注册六个面向模型的工具(sql_list / sql_query /
|
|
5
|
+
* sql_exec / sql_schema / sql_stats / sql_health),支持 SQLite / MySQL / PostgreSQL
|
|
6
|
+
* 三引擎与多连接。
|
|
6
7
|
* sql_exec 默认走宿主审批门(对齐 dsh-email 的发信审批),readOnly 模式可整体禁用写。
|
|
7
8
|
*
|
|
8
9
|
* @module dsh-sql
|
|
@@ -22,7 +23,7 @@ type SqlPreToolDecision = {
|
|
|
22
23
|
kind: 'ask';
|
|
23
24
|
reason?: string;
|
|
24
25
|
};
|
|
25
|
-
/** 审批策略需要读取的 alpha.
|
|
26
|
+
/** 审批策略需要读取的 alpha.5 工具执行字段。 */
|
|
26
27
|
interface SqlToolExecution {
|
|
27
28
|
readonly name: string;
|
|
28
29
|
readonly arguments: unknown;
|
|
@@ -38,7 +39,7 @@ export interface SqlPluginContext {
|
|
|
38
39
|
on(event: 'dispose', listener: () => void): () => void;
|
|
39
40
|
}
|
|
40
41
|
/**
|
|
41
|
-
*
|
|
42
|
+
* 插件入口:解析配置、构建六工具、给 sql_exec 注入审批门。
|
|
42
43
|
*/
|
|
43
44
|
export declare function apply(ctx: SqlPluginContext, config?: SqlConfig | null): void;
|
|
44
45
|
export * from './adapters.js';
|
package/lib/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-sql —— 工程师级数据库工具插件(node 半身,配置走 cordis.patch.yml)。
|
|
3
3
|
*
|
|
4
|
-
* 插件导出 apply(ctx, config)
|
|
5
|
-
* sql_exec / sql_schema),支持 SQLite / MySQL / PostgreSQL
|
|
4
|
+
* 插件导出 apply(ctx, config):注册六个面向模型的工具(sql_list / sql_query /
|
|
5
|
+
* sql_exec / sql_schema / sql_stats / sql_health),支持 SQLite / MySQL / PostgreSQL
|
|
6
|
+
* 三引擎与多连接。
|
|
6
7
|
* sql_exec 默认走宿主审批门(对齐 dsh-email 的发信审批),readOnly 模式可整体禁用写。
|
|
7
8
|
*
|
|
8
9
|
* @module dsh-sql
|
|
@@ -13,17 +14,10 @@ import { buildSqlTools } from './tools.js';
|
|
|
13
14
|
export const name = 'sql';
|
|
14
15
|
export const inject = ['tools'];
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
+
* 插件入口:解析配置、构建六工具、给 sql_exec 注入审批门。
|
|
17
18
|
*/
|
|
18
19
|
export function apply(ctx, config) {
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
cfg = resolveConfig(config);
|
|
22
|
-
}
|
|
23
|
-
catch (error) {
|
|
24
|
-
console.warn('[dsh-sql] ' + (error instanceof Error ? error.message : String(error)));
|
|
25
|
-
cfg = resolveConfig(null);
|
|
26
|
-
}
|
|
20
|
+
const cfg = resolveConfig(config);
|
|
27
21
|
const { tools, adapters } = buildSqlTools(cfg);
|
|
28
22
|
if (cfg.writeApproval) {
|
|
29
23
|
ctx.on('tools/pre-execute', async (exec, next) => {
|
package/lib/tools.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface SqlExecGateContext {
|
|
|
37
37
|
callId?: unknown;
|
|
38
38
|
signal?: unknown;
|
|
39
39
|
}
|
|
40
|
-
/**
|
|
40
|
+
/** 构建六个工具定义;adapters 惰性创建并按连接名缓存。 */
|
|
41
41
|
export declare function buildSqlTools(config: ResolvedSqlConfig): {
|
|
42
42
|
tools: SqlToolDefinition[];
|
|
43
43
|
adapters: Map<string, DatabaseAdapter>;
|
package/lib/tools.js
CHANGED
|
@@ -32,6 +32,12 @@ function requiredString(args, key, label) {
|
|
|
32
32
|
throw new Error(label + '(参数 ' + key + ')为必填,请提供非空字符串。');
|
|
33
33
|
return value;
|
|
34
34
|
}
|
|
35
|
+
function executionSignal(exec) {
|
|
36
|
+
if (typeof exec !== 'object' || exec === null)
|
|
37
|
+
return undefined;
|
|
38
|
+
const signal = exec.signal;
|
|
39
|
+
return signal instanceof AbortSignal ? signal : undefined;
|
|
40
|
+
}
|
|
35
41
|
/** 只读语句关键字白名单。 */
|
|
36
42
|
const READ_KEYWORDS = /^(select|pragma|explain|show|describe|desc|with)\b/i;
|
|
37
43
|
/** 去掉字符串、引号标识符与注释,保留真实 SQL 关键字与分号。 */
|
|
@@ -125,7 +131,7 @@ function stripSqlNoise(sql) {
|
|
|
125
131
|
return out;
|
|
126
132
|
}
|
|
127
133
|
/** 写操作关键字:出在 SELECT/EXPLAIN/WITH 语句里即拒绝。 */
|
|
128
|
-
const WRITE_KEYWORDS = /\b(insert|update|delete|replace|merge|drop|alter|create|truncate|call|execute|copy|grant|revoke|attach|detach|vacuum|reindex|refresh|set|reset|begin|commit|rollback|savepoint|release|analyze|load_extension)\b/
|
|
134
|
+
const WRITE_KEYWORDS = /\b(insert|update|delete|replace|merge|drop|alter|create|truncate|call|execute|copy|grant|revoke|attach|detach|vacuum|reindex|refresh|set|reset|begin|commit|rollback|savepoint|release|analyze|load_extension)\b/i;
|
|
129
135
|
/** 校验只读查询:词法去噪后白名单开头 + 写关键字扫描 + 单语句。 */
|
|
130
136
|
export function assertReadQuery(sql) {
|
|
131
137
|
const trimmed = sql.trim();
|
|
@@ -241,17 +247,17 @@ function formatBytes(bytes) {
|
|
|
241
247
|
return (bytes / 1024 / 1024 / 1024).toFixed(2) + ' GB';
|
|
242
248
|
}
|
|
243
249
|
/** 库体积:SQLite 用页数×页大小;MySQL/PostgreSQL 走系统函数;失败抛错由调用方兜底。 */
|
|
244
|
-
async function databaseSize(adapter, engine) {
|
|
250
|
+
async function databaseSize(adapter, engine, signal) {
|
|
245
251
|
if (engine === 'sqlite') {
|
|
246
|
-
const pageCount = await adapter.query('PRAGMA page_count', 1);
|
|
247
|
-
const pageSize = await adapter.query('PRAGMA page_size', 1);
|
|
252
|
+
const pageCount = await adapter.query('PRAGMA page_count', 1, signal);
|
|
253
|
+
const pageSize = await adapter.query('PRAGMA page_size', 1, signal);
|
|
248
254
|
return Number(pageCount.rows[0]?.[0] ?? 0) * Number(pageSize.rows[0]?.[0] ?? 0);
|
|
249
255
|
}
|
|
250
256
|
if (engine === 'mysql') {
|
|
251
|
-
const result = await adapter.query('SELECT SUM(DATA_LENGTH + INDEX_LENGTH) FROM information_schema.tables WHERE TABLE_SCHEMA = DATABASE()', 1);
|
|
257
|
+
const result = await adapter.query('SELECT SUM(DATA_LENGTH + INDEX_LENGTH) FROM information_schema.tables WHERE TABLE_SCHEMA = DATABASE()', 1, signal);
|
|
252
258
|
return Number(result.rows[0]?.[0] ?? -1);
|
|
253
259
|
}
|
|
254
|
-
const result = await adapter.query('SELECT pg_database_size(current_database())', 1);
|
|
260
|
+
const result = await adapter.query('SELECT pg_database_size(current_database())', 1, signal);
|
|
255
261
|
return Number(result.rows[0]?.[0] ?? -1);
|
|
256
262
|
}
|
|
257
263
|
const statsSchema = {
|
|
@@ -279,7 +285,7 @@ const healthSchema = {
|
|
|
279
285
|
},
|
|
280
286
|
additionalProperties: true,
|
|
281
287
|
};
|
|
282
|
-
/**
|
|
288
|
+
/** 构建六个工具定义;adapters 惰性创建并按连接名缓存。 */
|
|
283
289
|
export function buildSqlTools(config) {
|
|
284
290
|
const cfg = config;
|
|
285
291
|
const adapters = new Map();
|
|
@@ -296,7 +302,7 @@ export function buildSqlTools(config) {
|
|
|
296
302
|
}
|
|
297
303
|
return { adapter, name: connection.name };
|
|
298
304
|
};
|
|
299
|
-
const pingAllConnections = async () => {
|
|
305
|
+
const pingAllConnections = async (signal) => {
|
|
300
306
|
const rows = [];
|
|
301
307
|
for (const connection of cfg.connections) {
|
|
302
308
|
const entry = { name: connection.name, engine: connection.engine };
|
|
@@ -308,11 +314,12 @@ export function buildSqlTools(config) {
|
|
|
308
314
|
}
|
|
309
315
|
try {
|
|
310
316
|
const { adapter } = getAdapter(connection.name);
|
|
311
|
-
await adapter.ping();
|
|
317
|
+
await adapter.ping(signal);
|
|
312
318
|
entry.ok = true;
|
|
313
319
|
entry.error = '';
|
|
314
320
|
}
|
|
315
321
|
catch (error) {
|
|
322
|
+
signal?.throwIfAborted();
|
|
316
323
|
entry.ok = false;
|
|
317
324
|
entry.error = error instanceof Error ? error.message : String(error);
|
|
318
325
|
}
|
|
@@ -338,8 +345,8 @@ export function buildSqlTools(config) {
|
|
|
338
345
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
339
346
|
},
|
|
340
347
|
},
|
|
341
|
-
async execute() {
|
|
342
|
-
return { connections: await pingAllConnections() };
|
|
348
|
+
async execute(_rawArgs, exec) {
|
|
349
|
+
return { connections: await pingAllConnections(executionSignal(exec)) };
|
|
343
350
|
},
|
|
344
351
|
timeoutMs: 30000,
|
|
345
352
|
};
|
|
@@ -369,11 +376,11 @@ export function buildSqlTools(config) {
|
|
|
369
376
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
370
377
|
},
|
|
371
378
|
},
|
|
372
|
-
async execute(rawArgs) {
|
|
379
|
+
async execute(rawArgs, exec) {
|
|
373
380
|
const args = asRecord(rawArgs);
|
|
374
381
|
const sql = assertReadQuery(requiredString(args, 'sql', 'SQL 语句'));
|
|
375
382
|
const { adapter, name } = getAdapter(optionalString(args, 'connection'));
|
|
376
|
-
const result = await adapter.query(sql, cfg.maxRows + 1);
|
|
383
|
+
const result = await adapter.query(sql, cfg.maxRows + 1, executionSignal(exec));
|
|
377
384
|
const total = result.rows.length;
|
|
378
385
|
const rows = result.rows.slice(0, cfg.maxRows);
|
|
379
386
|
const format = optionalString(args, 'format')?.toLowerCase() ?? 'table';
|
|
@@ -409,13 +416,13 @@ export function buildSqlTools(config) {
|
|
|
409
416
|
return [{ type: 'text', text: '执行完成(' + rec.connection + '):影响 ' + rec.changes + ' 行。' }];
|
|
410
417
|
},
|
|
411
418
|
},
|
|
412
|
-
async execute(rawArgs) {
|
|
419
|
+
async execute(rawArgs, exec) {
|
|
413
420
|
if (cfg.readOnly)
|
|
414
421
|
throw new Error('当前配置 readOnly=true,sql_exec 已被禁用。需要写操作请把插件配置里的 readOnly 改为 false 后重启。');
|
|
415
422
|
const args = asRecord(rawArgs);
|
|
416
423
|
const sql = requiredString(args, 'sql', 'SQL 语句');
|
|
417
424
|
const { adapter, name } = getAdapter(optionalString(args, 'connection'));
|
|
418
|
-
const changes = await adapter.exec(sql);
|
|
425
|
+
const changes = await adapter.exec(sql, executionSignal(exec));
|
|
419
426
|
return { connection: name, changes, readOnly: false };
|
|
420
427
|
},
|
|
421
428
|
timeoutMs: cfg.execTimeoutMs,
|
|
@@ -444,15 +451,16 @@ export function buildSqlTools(config) {
|
|
|
444
451
|
return [{ type: 'text', text: '共 ' + tables.length + ' 张表:' + tables.join(', ') }];
|
|
445
452
|
},
|
|
446
453
|
},
|
|
447
|
-
async execute(rawArgs) {
|
|
454
|
+
async execute(rawArgs, exec) {
|
|
448
455
|
const args = asRecord(rawArgs);
|
|
449
456
|
const { adapter, name } = getAdapter(optionalString(args, 'connection'));
|
|
450
457
|
const table = optionalString(args, 'table');
|
|
458
|
+
const signal = executionSignal(exec);
|
|
451
459
|
if (table !== undefined) {
|
|
452
|
-
const columns = await adapter.describeTable(table);
|
|
460
|
+
const columns = await adapter.describeTable(table, signal);
|
|
453
461
|
return { connection: name, table, columns, tables: [] };
|
|
454
462
|
}
|
|
455
|
-
const tables = await adapter.listTables();
|
|
463
|
+
const tables = await adapter.listTables(signal);
|
|
456
464
|
return { connection: name, tables, columns: [] };
|
|
457
465
|
},
|
|
458
466
|
timeoutMs: 30000,
|
|
@@ -478,15 +486,17 @@ export function buildSqlTools(config) {
|
|
|
478
486
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
479
487
|
},
|
|
480
488
|
},
|
|
481
|
-
async execute(rawArgs) {
|
|
489
|
+
async execute(rawArgs, exec) {
|
|
482
490
|
const args = asRecord(rawArgs);
|
|
483
491
|
const { adapter, name } = getAdapter(optionalString(args, 'connection'));
|
|
484
492
|
const engine = adapter.engine;
|
|
493
|
+
const signal = executionSignal(exec);
|
|
485
494
|
let sizeBytes = -1;
|
|
486
495
|
try {
|
|
487
|
-
sizeBytes = await databaseSize(adapter, engine);
|
|
496
|
+
sizeBytes = await databaseSize(adapter, engine, signal);
|
|
488
497
|
}
|
|
489
498
|
catch {
|
|
499
|
+
signal?.throwIfAborted();
|
|
490
500
|
sizeBytes = -1;
|
|
491
501
|
}
|
|
492
502
|
const tables = [];
|
|
@@ -495,23 +505,25 @@ export function buildSqlTools(config) {
|
|
|
495
505
|
const sql = engine === 'mysql'
|
|
496
506
|
? 'SELECT TABLE_NAME, TABLE_ROWS FROM information_schema.tables WHERE TABLE_SCHEMA = DATABASE()'
|
|
497
507
|
: 'SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY relname';
|
|
498
|
-
const result = await adapter.query(sql);
|
|
508
|
+
const result = await adapter.query(sql, undefined, signal);
|
|
499
509
|
for (const row of result.rows) {
|
|
500
510
|
tables.push({ name: String(row[0]), rowCount: typeof row[1] === 'number' ? row[1] : Number(row[1] ?? 0) });
|
|
501
511
|
}
|
|
502
512
|
}
|
|
503
513
|
catch (error) {
|
|
514
|
+
signal?.throwIfAborted();
|
|
504
515
|
tables.push({ name: '', error: error instanceof Error ? error.message : String(error) });
|
|
505
516
|
}
|
|
506
517
|
}
|
|
507
518
|
else {
|
|
508
|
-
const names = await adapter.listTables();
|
|
519
|
+
const names = await adapter.listTables(signal);
|
|
509
520
|
for (const table of names) {
|
|
510
521
|
try {
|
|
511
|
-
const result = await adapter.query('SELECT COUNT(*) FROM ' + quoteIdent(engine, table), 1);
|
|
522
|
+
const result = await adapter.query('SELECT COUNT(*) FROM ' + quoteIdent(engine, table), 1, signal);
|
|
512
523
|
tables.push({ name: table, rowCount: Number(result.rows[0]?.[0] ?? 0) });
|
|
513
524
|
}
|
|
514
525
|
catch (error) {
|
|
526
|
+
signal?.throwIfAborted();
|
|
515
527
|
tables.push({ name: table, error: error instanceof Error ? error.message : String(error) });
|
|
516
528
|
}
|
|
517
529
|
}
|
|
@@ -541,8 +553,8 @@ export function buildSqlTools(config) {
|
|
|
541
553
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
542
554
|
},
|
|
543
555
|
},
|
|
544
|
-
async execute() {
|
|
545
|
-
const connections = await pingAllConnections();
|
|
556
|
+
async execute(_rawArgs, exec) {
|
|
557
|
+
const connections = await pingAllConnections(executionSignal(exec));
|
|
546
558
|
const bad = connections.filter((c) => c.ok !== true);
|
|
547
559
|
return {
|
|
548
560
|
ok: bad.length === 0,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sql",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "DSH 工程师级数据库插件:sql_list/query/exec/schema
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "DSH 工程师级数据库插件:sql_list/query/exec/schema/stats/health 六工具,SQLite/MySQL/PostgreSQL 三引擎、多连接、只读模式与写操作审批门、词法级只读保护、流式行数钳制与可配置查询/写超时。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"types": "lib/index.d.ts",
|