dsh-sql 0.5.3 → 0.5.4
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/CHANGELOG.md +7 -0
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/docs/USAGE.md +2 -1
- package/docs/VALIDATION.md +8 -0
- package/lib/adapters.d.ts +2 -3
- package/lib/adapters.js +67 -27
- package/lib/tools.js +1 -1
- package/lib/update-client.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[返回简介](README.md) · [使用说明](docs/USAGE.md) · [验证记录](docs/VALIDATION.md)
|
|
4
4
|
|
|
5
|
+
## 0.5.4 (2026-10-28)
|
|
6
|
+
|
|
7
|
+
- 修复 PostgreSQL 查询能修改会话默认设置,再通过写函数绕过只读保护的问题。每次查询强制使用独立只读事务并回滚;允许审批写入时查询工具仍保持只读。
|
|
8
|
+
- PostgreSQL 查询使用扩展协议,服务器拒绝同一请求中的多条命令。MySQL 同步等待只读事务建立,设置失败时拒绝查询;不再忽略连接初始化错误。
|
|
9
|
+
- 查询成功或失败后回滚;取消或达到行数上限时销毁连接;回滚失败的连接不放回池中。保留行数限制、同名列和 64 位整数处理。
|
|
10
|
+
- 增加真实 PostgreSQL 可选回归,覆盖设置修改、写函数、多语句、截流、取消与合法审批写入;默认测试不启动或下载数据库。
|
|
11
|
+
|
|
5
12
|
## 0.5.3 (2026-10-27)
|
|
6
13
|
|
|
7
14
|
- 修复分批更新时的新旧更新控件混用:保留原页面、草稿与安装保护,空闲后接管旧控制器,兼容旧入口。
|
package/README.en.md
CHANGED
|
@@ -13,7 +13,7 @@ Feedback and contributions are welcome: report [issues](https://github.com/STARD
|
|
|
13
13
|
## What it does
|
|
14
14
|
|
|
15
15
|
- Manage connections and inspect schemas or database summaries.
|
|
16
|
-
- Limit rows and timeouts for read-only queries.
|
|
16
|
+
- Limit rows and timeouts for read-only queries; MySQL / PostgreSQL enforce a separate read-only transaction for each query.
|
|
17
17
|
- Keep writes under host approval and check connections.
|
|
18
18
|
|
|
19
19
|
## Install
|
package/README.md
CHANGED
package/docs/USAGE.md
CHANGED
|
@@ -77,7 +77,8 @@ sql_exec { sql: UPDATE orders SET status = 'paid' WHERE id = 42 }
|
|
|
77
77
|
|
|
78
78
|
- **词法级只读保护**:sql_query 先剥离字符串/注释再校验,拒绝 data-modifying CTE(WITH…DELETE/UPDATE)、SELECT INTO、FOR UPDATE/FOR SHARE、多语句,以及不在只读白名单里的 PRAGMA。去噪按引擎处理反斜杠(MySQL 默认转义、PostgreSQL 只有 `E'…'`/`U&'…'` 转义、SQLite 与引号标识符里都不转义),避免「校验看到一条、服务器执行两条」
|
|
79
79
|
- **写审批门**:sql_exec 默认弹审批(对齐 dsh-email 的发信审批),headless 环境无审批通道时拒绝执行
|
|
80
|
-
-
|
|
80
|
+
- **查询事务**:MySQL / PostgreSQL 的 `sql_query` 每次在独立只读事务中运行,完成或报错后回滚,达到行数上限或取消时销毁专用连接。PostgreSQL 查询采用扩展协议,服务器拒绝同一请求内的多条命令;`set_config` 对默认设置的修改随回滚撤销。MySQL 必须成功执行 `START TRANSACTION READ ONLY`,不支持或设置失败时拒绝运行查询。即使允许审批写入,这些查询约束仍然生效。
|
|
81
|
+
- **readOnly 模式**:生产库可整体禁用 `sql_exec`。SQLite 文件以只读方式打开;PostgreSQL 还保留只读启动默认值。建议使用仅授予所需读取权限的数据库账号;事务只读不等同于数据库账号、文件系统或外部函数的权限隔离。
|
|
81
82
|
- **审批卡不藏语句**:sql_exec 的审批文本逐条列出每条语句,并给出一行语句类型统计;超过 800 字符时把完整 SQL 指回工具参数,第二条 DROP / DELETE 不会因为截断而从审批卡里消失
|
|
82
83
|
- **流式行数钳制**:SQLite 迭代器 / MySQL Readable / PostgreSQL Query 行事件最多收集 maxRows+1 行,超量标记 truncated;MySQL 和 PostgreSQL 在达到上限时关闭该查询的专用连接,未达上限则正常归还连接池,避免全量结果驻留内存
|
|
83
84
|
- **可取消执行**:查询与写操作遵守 Harness 的 `exec.signal`;取消时会中止等待并销毁正在工作的 MySQL/PostgreSQL 专用连接
|
package/docs/VALIDATION.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
|
|
4
4
|
|
|
5
|
+
## 0.5.4(2026-10-28)
|
|
6
|
+
|
|
7
|
+
- Windows / Node 24.16.0:71 项检查通过,包含真实 PostgreSQL 17.10 的可选集成回归。测试数据库、表、函数与账号均为隔离夹具。
|
|
8
|
+
- 已在 0.5.3 复现:查询修改 `default_transaction_read_only` 后,后续 SELECT 写函数能落盘。0.5.4 在同一真实数据库拒绝该写入,查询始终处于只读事务。
|
|
9
|
+
- 真实 PostgreSQL 覆盖 readOnly 开关两种状态、有界和无界查询、修改当前事务设置、多命令拒绝、写函数拒绝、错误后恢复、行数截流、取消以及合法 exec 写入。MySQL 只验证真实 mysql2 流事件与连接生命周期夹具,未连接真实 MySQL 服务。
|
|
10
|
+
- 官方 DSH 0.2.1-rc.3 原生网页版:工具入口拒绝原 Issue #3 的多语句和 PRAGMA 写入;浏览器审批卡显示 200 字符后面的 DROP,点击拒绝后真实 SQLite 数据保持不变。浏览器使用后台 Chrome,未验收 Electron 原生窗口。
|
|
11
|
+
- 运行可选 PostgreSQL 回归:在专用测试环境中把连接对象 JSON 写入环境变量 `DSH_SQL_TEST_POSTGRES`,再执行 `pnpm test`;未设置时明确跳过该集成项,不启动或下载数据库。
|
|
12
|
+
|
|
5
13
|
## 原中文记录
|
|
6
14
|
|
|
7
15
|
验证宿主:官方源码构建的 Harness `0.2.0-rc.1`(commit `407e65c8`)+ Node `24.16.0`(2026-09-28)。53 项插件测试在隔离环境全部通过;同一个宿主里 18 个插件共同加载,注册 6 个工具,工具 schema 与健康检查契约通过。本轮未启用真实端口与外部服务。
|
package/lib/adapters.d.ts
CHANGED
|
@@ -30,9 +30,8 @@ export interface AdapterOptions {
|
|
|
30
30
|
/** MySQL 连接池配置(导出以便测试断言,不含连接)。 */
|
|
31
31
|
export declare function mysqlPoolConfig(connection: SqlConnectionConfig): mysql.PoolOptions;
|
|
32
32
|
/**
|
|
33
|
-
* PostgreSQL
|
|
34
|
-
*
|
|
35
|
-
* "cannot execute ... in a read-only transaction" 拒绝写操作。
|
|
33
|
+
* PostgreSQL 连接池配置。启动参数仅作为默认值;用户查询还必须使用
|
|
34
|
+
* 独立只读事务,不能依赖可被 set_config 修改的会话默认设置。
|
|
36
35
|
*/
|
|
37
36
|
export declare function postgresPoolConfig(connection: SqlConnectionConfig, readOnly?: boolean): pg.PoolConfig;
|
|
38
37
|
/** 按连接配置创建适配器;readOnly 会下沉到引擎自身的只读约束。 */
|
package/lib/adapters.js
CHANGED
|
@@ -83,7 +83,8 @@ function streamPostgresQuery(client, sql, limit, discard) {
|
|
|
83
83
|
let settled = false;
|
|
84
84
|
let columns = [];
|
|
85
85
|
const rows = [];
|
|
86
|
-
|
|
86
|
+
// The extended protocol rejects multiple commands in a single Parse message.
|
|
87
|
+
const queryConfig = { text: sql, rowMode: 'array', queryMode: 'extended' };
|
|
87
88
|
const query = new pg.Query(queryConfig);
|
|
88
89
|
query.on('row', (row, result) => {
|
|
89
90
|
if (settled)
|
|
@@ -134,9 +135,8 @@ export function mysqlPoolConfig(connection) {
|
|
|
134
135
|
};
|
|
135
136
|
}
|
|
136
137
|
/**
|
|
137
|
-
* PostgreSQL
|
|
138
|
-
*
|
|
139
|
-
* "cannot execute ... in a read-only transaction" 拒绝写操作。
|
|
138
|
+
* PostgreSQL 连接池配置。启动参数仅作为默认值;用户查询还必须使用
|
|
139
|
+
* 独立只读事务,不能依赖可被 set_config 修改的会话默认设置。
|
|
140
140
|
*/
|
|
141
141
|
export function postgresPoolConfig(connection, readOnly = false) {
|
|
142
142
|
const config = {
|
|
@@ -230,17 +230,12 @@ class SqliteAdapter {
|
|
|
230
230
|
}
|
|
231
231
|
/** MySQL 适配器(mysql2 连接池)。 */
|
|
232
232
|
class MysqlAdapter {
|
|
233
|
+
readOnly;
|
|
233
234
|
engine = 'mysql';
|
|
234
235
|
pool;
|
|
235
236
|
constructor(connection, readOnly = false) {
|
|
237
|
+
this.readOnly = readOnly;
|
|
236
238
|
this.pool = mysql.createPool(mysqlPoolConfig(connection));
|
|
237
|
-
if (readOnly) {
|
|
238
|
-
// 会话级只读:MySQL 5.6.5+ 的 SET SESSION TRANSACTION READ ONLY 会让
|
|
239
|
-
// 该连接上后续任何 DML/DDL 被服务器拒绝,不依赖词法校验。
|
|
240
|
-
this.pool.on('connection', (session) => {
|
|
241
|
-
void session.query('SET SESSION TRANSACTION READ ONLY').catch(() => { });
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
239
|
}
|
|
245
240
|
async withSignalConnection(signal, work) {
|
|
246
241
|
signal?.throwIfAborted();
|
|
@@ -261,7 +256,7 @@ class MysqlAdapter {
|
|
|
261
256
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
262
257
|
try {
|
|
263
258
|
signal?.throwIfAborted();
|
|
264
|
-
return await Promise.race([work(connection, discard), aborted]);
|
|
259
|
+
return await Promise.race([work(connection, discard, () => destroyed), aborted]);
|
|
265
260
|
}
|
|
266
261
|
finally {
|
|
267
262
|
signal?.removeEventListener('abort', onAbort);
|
|
@@ -269,6 +264,26 @@ class MysqlAdapter {
|
|
|
269
264
|
connection.release();
|
|
270
265
|
}
|
|
271
266
|
}
|
|
267
|
+
async withReadOnlyConnection(signal, work) {
|
|
268
|
+
return await this.withSignalConnection(signal, async (connection, discard, isDiscarded) => {
|
|
269
|
+
try {
|
|
270
|
+
// Await setup and fail closed; never silently ignore an unsupported mode.
|
|
271
|
+
await connection.query('START TRANSACTION READ ONLY');
|
|
272
|
+
return await work(connection, discard);
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
if (!isDiscarded()) {
|
|
276
|
+
try {
|
|
277
|
+
await connection.query('ROLLBACK');
|
|
278
|
+
}
|
|
279
|
+
catch (error) {
|
|
280
|
+
discard();
|
|
281
|
+
throw error;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
272
287
|
async queryRows(sql, signal) {
|
|
273
288
|
if (signal === undefined)
|
|
274
289
|
return typeof sql === 'string' ? await this.pool.query(sql) : await this.pool.query(sql);
|
|
@@ -289,18 +304,20 @@ class MysqlAdapter {
|
|
|
289
304
|
}));
|
|
290
305
|
}
|
|
291
306
|
async query(sql, limit, signal) {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
307
|
+
return await this.withReadOnlyConnection(signal, async (connection, discard) => {
|
|
308
|
+
if (limit === undefined || limit <= 0) {
|
|
309
|
+
const [rows, fields] = await connection.query({ sql, rowsAsArray: true });
|
|
310
|
+
if (fields?.length)
|
|
311
|
+
return { columns: fields.map(field => field.name), rows: rows.map(row => Array.isArray(row) ? row.map(toValue) : fields.map(field => toValue(row[field.name]))) };
|
|
312
|
+
return rowsToColumns(rows);
|
|
313
|
+
}
|
|
299
314
|
const coreConnection = connection.connection;
|
|
300
315
|
return await streamMysqlQuery(coreConnection, sql, limit, discard);
|
|
301
316
|
});
|
|
302
317
|
}
|
|
303
318
|
async exec(sql, signal) {
|
|
319
|
+
if (this.readOnly)
|
|
320
|
+
throw new Error('当前配置 readOnly=true,禁止写入数据库。');
|
|
304
321
|
const [result] = await this.queryRows(sql, signal);
|
|
305
322
|
return Number(result?.affectedRows ?? 0);
|
|
306
323
|
}
|
|
@@ -313,9 +330,11 @@ class MysqlAdapter {
|
|
|
313
330
|
}
|
|
314
331
|
/** PostgreSQL 适配器(pg 连接池)。 */
|
|
315
332
|
class PostgresAdapter {
|
|
333
|
+
readOnly;
|
|
316
334
|
engine = 'postgres';
|
|
317
335
|
pool;
|
|
318
336
|
constructor(connection, readOnly = false) {
|
|
337
|
+
this.readOnly = readOnly;
|
|
319
338
|
this.pool = new pg.Pool(postgresPoolConfig(connection, readOnly));
|
|
320
339
|
}
|
|
321
340
|
async withSignalClient(signal, work) {
|
|
@@ -337,7 +356,7 @@ class PostgresAdapter {
|
|
|
337
356
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
338
357
|
try {
|
|
339
358
|
signal?.throwIfAborted();
|
|
340
|
-
return await Promise.race([work(client, discard), aborted]);
|
|
359
|
+
return await Promise.race([work(client, discard, () => destroyed), aborted]);
|
|
341
360
|
}
|
|
342
361
|
finally {
|
|
343
362
|
signal?.removeEventListener('abort', onAbort);
|
|
@@ -345,6 +364,27 @@ class PostgresAdapter {
|
|
|
345
364
|
client.release();
|
|
346
365
|
}
|
|
347
366
|
}
|
|
367
|
+
async withReadOnlyClient(signal, work) {
|
|
368
|
+
return await this.withSignalClient(signal, async (client, discard, isDiscarded) => {
|
|
369
|
+
try {
|
|
370
|
+
await client.query('BEGIN READ ONLY');
|
|
371
|
+
return await work(client, discard);
|
|
372
|
+
}
|
|
373
|
+
finally {
|
|
374
|
+
// Rollback also reverts set_config(..., false). A capped/cancelled query
|
|
375
|
+
// destroys its connection instead, causing server-side rollback.
|
|
376
|
+
if (!isDiscarded()) {
|
|
377
|
+
try {
|
|
378
|
+
await client.query('ROLLBACK');
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
discard();
|
|
382
|
+
throw error;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
348
388
|
async queryWithSignal(query, values, signal) {
|
|
349
389
|
const run = async (client) => {
|
|
350
390
|
return values === undefined ? await client.query(query) : await client.query(query, values);
|
|
@@ -383,18 +423,18 @@ class PostgresAdapter {
|
|
|
383
423
|
}));
|
|
384
424
|
}
|
|
385
425
|
async query(sql, limit, signal) {
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
426
|
+
return await this.withReadOnlyClient(signal, async (client, discard) => {
|
|
427
|
+
if (limit === undefined || limit <= 0) {
|
|
428
|
+
const queryConfig = { text: sql, rowMode: 'array', queryMode: 'extended' };
|
|
429
|
+
const result = await client.query(queryConfig);
|
|
389
430
|
return { columns: result.fields.map((field) => field.name), rows: result.rows.map((row) => row.map(toValue)) };
|
|
390
|
-
|
|
391
|
-
return rowsToColumns(rows);
|
|
392
|
-
}
|
|
393
|
-
return await this.withSignalClient(signal, async (client, discard) => {
|
|
431
|
+
}
|
|
394
432
|
return await streamPostgresQuery(client, sql, limit, discard);
|
|
395
433
|
});
|
|
396
434
|
}
|
|
397
435
|
async exec(sql, signal) {
|
|
436
|
+
if (this.readOnly)
|
|
437
|
+
throw new Error('当前配置 readOnly=true,禁止写入数据库。');
|
|
398
438
|
const result = await this.queryWithSignal(sql, undefined, signal);
|
|
399
439
|
return Number(result.rowCount ?? 0);
|
|
400
440
|
}
|
package/lib/tools.js
CHANGED
|
@@ -473,7 +473,7 @@ export function buildSqlTools(config) {
|
|
|
473
473
|
};
|
|
474
474
|
const sqlQuery = {
|
|
475
475
|
name: 'sql_query',
|
|
476
|
-
description: '执行只读 SQL 查询(SELECT / PRAGMA / EXPLAIN / SHOW / DESCRIBE / WITH)。词法级校验按引擎处理反斜杠转义,拒绝 data-modifying CTE、SELECT INTO、FOR UPDATE/FOR SHARE、非白名单 PRAGMA
|
|
476
|
+
description: '执行只读 SQL 查询(SELECT / PRAGMA / EXPLAIN / SHOW / DESCRIBE / WITH)。词法级校验按引擎处理反斜杠转义,拒绝 data-modifying CTE、SELECT INTO、FOR UPDATE/FOR SHARE、非白名单 PRAGMA 与多语句。MySQL / PostgreSQL 每次查询使用独立只读事务并回滚,不受 readOnly 开关影响;PostgreSQL 还以扩展协议拒绝多命令。connection 为连接名(缺省第一个连接)。返回列名与行数据,最多 maxRows 行(超出 truncated=true)。写操作请用 sql_exec。',
|
|
477
477
|
parameters: compileParameters({
|
|
478
478
|
sql: { type: 'string', required: true, description: '只读 SQL 语句(必填,单条)。' },
|
|
479
479
|
connection: { type: 'string', description: '连接名(可选,缺省第一个连接)。' },
|
package/lib/update-client.js
CHANGED
|
@@ -437,7 +437,7 @@ function __stardustUpdatesClient(ctx, React, require, shared) {
|
|
|
437
437
|
}
|
|
438
438
|
|
|
439
439
|
/* STARDUSTLC_UPDATE_CLIENT_BODY_START */
|
|
440
|
-
window.__ModuleLoader__.load(__stardustUpdatesWrap({id:"dsh-sql",factory:require=>({apply(){}})},{"name":"dsh-sql","version":"0.1.0","hasWorkbench":false,"packageVersion":"0.5.
|
|
440
|
+
window.__ModuleLoader__.load(__stardustUpdatesWrap({id:"dsh-sql",factory:require=>({apply(){}})},{"name":"dsh-sql","version":"0.1.0","hasWorkbench":false,"packageVersion":"0.5.4","label":"数据库","labelEn":"SQL","suiteComponent":true}));
|
|
441
441
|
|
|
442
442
|
/* STARDUSTLC_UPDATE_CLIENT_BODY_END */
|
|
443
443
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-sql",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "连接 SQLite、MySQL 或 PostgreSQL,查询数据并检查数据库结构。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"mysql2": "^3.11.0",
|
|
62
|
-
"pg": "^8.
|
|
62
|
+
"pg": "^8.23.0",
|
|
63
63
|
"@stardustlc/dsh-updates": "0.1.3"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|