dsh-sql 0.5.8 → 0.5.10
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 +12 -0
- package/README.en.md +35 -24
- package/README.md +33 -23
- package/docs/REFERENCE.en.md +61 -0
- package/docs/REFERENCE.zh.md +61 -0
- package/docs/USAGE.en.md +17 -0
- package/docs/USAGE.md +17 -0
- package/docs/assets/intro/workflow-en-dark.svg +1 -0
- package/docs/assets/intro/workflow-en-light.svg +1 -0
- package/docs/assets/intro/workflow-en-mobile-dark.svg +1 -0
- package/docs/assets/intro/workflow-en-mobile-light.svg +1 -0
- package/docs/assets/intro/workflow-zh-dark.svg +1 -0
- package/docs/assets/intro/workflow-zh-light.svg +1 -0
- package/docs/assets/intro/workflow-zh-mobile-dark.svg +1 -0
- package/docs/assets/intro/workflow-zh-mobile-light.svg +1 -0
- package/lib/tools.d.ts +10 -0
- package/lib/tools.js +86 -11
- package/lib/update-client.js +1 -1
- package/package.json +3 -3
package/lib/tools.js
CHANGED
|
@@ -38,6 +38,42 @@ function executionSignal(exec) {
|
|
|
38
38
|
const signal = exec.signal;
|
|
39
39
|
return signal instanceof AbortSignal ? signal : undefined;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* 把驱动/连接的原始错误映射成一行可执行的「下一步」。
|
|
43
|
+
*
|
|
44
|
+
* 之前 sql_list / sql_health / sql_query 直接把驱动原文(ECONNREFUSED、
|
|
45
|
+
* ER_ACCESS_DENIED_ERROR、SQLITE_CANTOPEN…)交给用户,知道怎么修的人能看懂,
|
|
46
|
+
* 不知道的人只能反复重试。这里只补一行提示,原始错误文本一律保留。
|
|
47
|
+
*/
|
|
48
|
+
export function connectionErrorHint(message) {
|
|
49
|
+
if (/ECONNREFUSED/i.test(message))
|
|
50
|
+
return '目标地址拒绝连接:请确认数据库服务已启动、host 与 port 正确,并检查防火墙是否放行。';
|
|
51
|
+
if (/ENOTFOUND|EAI_AGAIN|getaddrinfo/i.test(message))
|
|
52
|
+
return '主机名解析失败:请检查连接配置里的 host 拼写,以及本机 DNS/网络是否可用。';
|
|
53
|
+
if (/ETIMEDOUT|timed out|timeout exceeded|connection timeout/i.test(message))
|
|
54
|
+
return '连接或查询超时:请确认网络可达与服务器负载,必要时调大 queryTimeoutMs / execTimeoutMs 后重试。';
|
|
55
|
+
if (/password authentication failed|Access denied for user|authentication failed/i.test(message))
|
|
56
|
+
return '认证失败:请核对 user 与 password;也可改用环境变量 DSH_SQL_PASSWORD_<连接名大写> 提供密码,避免明文写进配置。';
|
|
57
|
+
if (/Unknown database|ER_BAD_DB_ERROR|database .* does not exist|\b3D000\b/i.test(message))
|
|
58
|
+
return '数据库不存在:请核对连接配置里的 database,或先在服务器上创建该库。';
|
|
59
|
+
if (/no such table|SQLITE_ERROR: no such table|relation .* does not exist|\b42P01\b/i.test(message))
|
|
60
|
+
return '表不存在:先用 sql_schema(不带 table)查看该连接的表清单,注意大小写与实际 schema。';
|
|
61
|
+
if (/SQLITE_CANTOPEN|unable to open database file|EACCES|EPERM|readonly database/i.test(message))
|
|
62
|
+
return 'SQLite 文件无法打开或没有权限:请检查文件路径是否存在、所在目录可写、进程账号有读写权限。';
|
|
63
|
+
if (/ECONNRESET|EPIPE|Connection terminated|server closed the connection/i.test(message))
|
|
64
|
+
return '连接被对端关闭:可能是服务器重启或中间代理断开,请重试一次;持续出现时检查连接池与代理设置。';
|
|
65
|
+
if (/no pg_hba\.conf entry|unsupported startup parameter|\b28000\b/i.test(message))
|
|
66
|
+
return '服务器拒绝本次连接:请核对 pg_hba.conf 与账号来源限制,确认该主机与该用户被允许登录。';
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
/** 给错误补一行「下一步」;无法识别时原样返回,避免把断言类错误改写成连接问题。 */
|
|
70
|
+
export function annotateError(error) {
|
|
71
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
72
|
+
const hint = connectionErrorHint(message);
|
|
73
|
+
if (hint === undefined || message.includes('下一步:'))
|
|
74
|
+
return error instanceof Error ? error : new Error(message);
|
|
75
|
+
return new Error(message + '\n下一步:' + hint);
|
|
76
|
+
}
|
|
41
77
|
/** 只读语句关键字白名单。 */
|
|
42
78
|
const READ_KEYWORDS = /^(select|pragma|explain|show|describe|desc|with)\b/i;
|
|
43
79
|
/**
|
|
@@ -309,7 +345,7 @@ const listSchema = {
|
|
|
309
345
|
type: 'array',
|
|
310
346
|
items: {
|
|
311
347
|
type: 'object',
|
|
312
|
-
properties: { name: { type: 'string' }, engine: { type: 'string' }, host: { type: 'string' }, database: { type: 'string' }, file: { type: 'string' }, ok: { type: 'boolean' }, error: { type: 'string' } },
|
|
348
|
+
properties: { name: { type: 'string' }, engine: { type: 'string' }, host: { type: 'string' }, port: { type: 'integer' }, database: { type: 'string' }, file: { type: 'string' }, ok: { type: 'boolean' }, error: { type: 'string' } },
|
|
313
349
|
additionalProperties: true,
|
|
314
350
|
},
|
|
315
351
|
},
|
|
@@ -423,6 +459,16 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
423
459
|
}
|
|
424
460
|
return { adapter, name: connection.name };
|
|
425
461
|
};
|
|
462
|
+
/** 适配器调用统一兜底:被取消时原样抛出,其余错误补「下一步」。 */
|
|
463
|
+
const guard = async (signal, work) => {
|
|
464
|
+
try {
|
|
465
|
+
return await work();
|
|
466
|
+
}
|
|
467
|
+
catch (error) {
|
|
468
|
+
signal?.throwIfAborted();
|
|
469
|
+
throw annotateError(error);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
426
472
|
const pingAllConnections = async (signal) => {
|
|
427
473
|
const rows = [];
|
|
428
474
|
for (const connection of cfg.connections) {
|
|
@@ -431,6 +477,8 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
431
477
|
entry.file = connection.file ?? ':memory:';
|
|
432
478
|
else {
|
|
433
479
|
entry.host = connection.host ?? '';
|
|
480
|
+
// 端口要一并返回,否则渲染只能显示 host:undefined/database。
|
|
481
|
+
entry.port = connection.port;
|
|
434
482
|
entry.database = connection.database ?? '';
|
|
435
483
|
}
|
|
436
484
|
try {
|
|
@@ -442,7 +490,7 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
442
490
|
catch (error) {
|
|
443
491
|
signal?.throwIfAborted();
|
|
444
492
|
entry.ok = false;
|
|
445
|
-
entry.error = error
|
|
493
|
+
entry.error = annotateError(error).message;
|
|
446
494
|
}
|
|
447
495
|
rows.push(entry);
|
|
448
496
|
}
|
|
@@ -458,10 +506,16 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
458
506
|
const rec = asRecord(value);
|
|
459
507
|
const connections = Array.isArray(rec.connections) ? rec.connections : [];
|
|
460
508
|
const lines = ['共 ' + connections.length + ' 个数据库连接:'];
|
|
509
|
+
const bad = [];
|
|
461
510
|
for (const item of connections) {
|
|
462
511
|
const c = asRecord(item);
|
|
463
|
-
const target = c.file !== undefined && c.file !== '' ? c.file : c.host + '/' + c.database;
|
|
512
|
+
const target = c.file !== undefined && c.file !== '' ? c.file : c.host + ':' + c.port + '/' + c.database;
|
|
464
513
|
lines.push('- ' + c.name + '(' + c.engine + ' @ ' + target + ')' + (c.ok === true ? ' ✅' : ' ❌ ' + String(c.error ?? '')));
|
|
514
|
+
if (c.ok !== true)
|
|
515
|
+
bad.push(String(c.name));
|
|
516
|
+
}
|
|
517
|
+
if (bad.length > 0) {
|
|
518
|
+
lines.push('', '下一步:先修好 ' + bad.join('、') + '(核对 host/port、服务是否启动、账号密码或 SQLite 文件路径),再运行 sql_list 复查。');
|
|
465
519
|
}
|
|
466
520
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
467
521
|
},
|
|
@@ -504,7 +558,7 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
504
558
|
// 先按目标连接的引擎做词法校验,再打开数据库:被拒绝的 SQL 不该去建连接或建库文件。
|
|
505
559
|
const sql = assertReadQuery(rawSql, findConnection(connectionName).engine);
|
|
506
560
|
const { adapter, name } = getAdapter(connectionName);
|
|
507
|
-
const result = await adapter.query(sql, cfg.maxRows + 1, executionSignal(exec));
|
|
561
|
+
const result = await guard(executionSignal(exec), () => adapter.query(sql, cfg.maxRows + 1, executionSignal(exec)));
|
|
508
562
|
const total = result.rows.length;
|
|
509
563
|
const rows = result.rows.slice(0, cfg.maxRows);
|
|
510
564
|
const format = optionalString(args, 'format')?.toLowerCase() ?? 'table';
|
|
@@ -540,7 +594,9 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
540
594
|
schema: execSchema,
|
|
541
595
|
render: (_args, value) => {
|
|
542
596
|
const rec = asRecord(value);
|
|
543
|
-
|
|
597
|
+
const changes = typeof rec.changes === 'number' ? rec.changes : 0;
|
|
598
|
+
const tail = changes === 0 ? '\n注意:0 行受影响,通常是 WHERE 条件没有命中数据,或用的是 DDL/多语句脚本;可用 sql_query 复核。' : '';
|
|
599
|
+
return [{ type: 'text', text: '执行完成(' + rec.connection + '):影响 ' + changes + ' 行。' + tail }];
|
|
544
600
|
},
|
|
545
601
|
},
|
|
546
602
|
async execute(rawArgs, exec) {
|
|
@@ -549,7 +605,7 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
549
605
|
const args = asRecord(rawArgs);
|
|
550
606
|
const sql = requiredString(args, 'sql', 'SQL 语句');
|
|
551
607
|
const { adapter, name } = getAdapter(optionalString(args, 'connection'));
|
|
552
|
-
const changes = await adapter.exec(sql, executionSignal(exec));
|
|
608
|
+
const changes = await guard(executionSignal(exec), () => adapter.exec(sql, executionSignal(exec)));
|
|
553
609
|
return { connection: name, changes, readOnly: false };
|
|
554
610
|
},
|
|
555
611
|
timeoutMs: cfg.execTimeoutMs,
|
|
@@ -566,7 +622,11 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
566
622
|
render: (_args, value) => {
|
|
567
623
|
const rec = asRecord(value);
|
|
568
624
|
const columns = Array.isArray(rec.columns) ? rec.columns : [];
|
|
569
|
-
|
|
625
|
+
// 指定了 table 却没有列:不能落到「共 0 张表」,那会把「表不存在」说成「库是空的」。
|
|
626
|
+
if (typeof rec.table === 'string' && rec.table !== '') {
|
|
627
|
+
if (columns.length === 0) {
|
|
628
|
+
return [{ type: 'text', text: '没有取到表 ' + rec.table + ' 的列:该表可能不存在、大小写/Schema 不匹配,或当前账号没有读取权限。\n下一步:先调用 sql_schema(不带 table)查看连接 ' + rec.connection + ' 的表清单,再用清单里的原名重试。' }];
|
|
629
|
+
}
|
|
570
630
|
const lines = ['表 ' + rec.table + ' 的列:'];
|
|
571
631
|
for (const item of columns) {
|
|
572
632
|
const c = asRecord(item);
|
|
@@ -575,6 +635,8 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
575
635
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
576
636
|
}
|
|
577
637
|
const tables = Array.isArray(rec.tables) ? rec.tables : [];
|
|
638
|
+
if (tables.length === 0)
|
|
639
|
+
return [{ type: 'text', text: '连接 ' + rec.connection + ' 里没有用户表(库可能是空的,或该账号只能看到部分 Schema)。' }];
|
|
578
640
|
return [{ type: 'text', text: '共 ' + tables.length + ' 张表:' + tables.join(', ') }];
|
|
579
641
|
},
|
|
580
642
|
},
|
|
@@ -584,10 +646,10 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
584
646
|
const table = optionalString(args, 'table');
|
|
585
647
|
const signal = executionSignal(exec);
|
|
586
648
|
if (table !== undefined) {
|
|
587
|
-
const columns = await adapter.describeTable(table, signal);
|
|
649
|
+
const columns = await guard(signal, () => adapter.describeTable(table, signal));
|
|
588
650
|
return { connection: name, table, columns, tables: [] };
|
|
589
651
|
}
|
|
590
|
-
const tables = await adapter.listTables(signal);
|
|
652
|
+
const tables = await guard(signal, () => adapter.listTables(signal));
|
|
591
653
|
return { connection: name, tables, columns: [] };
|
|
592
654
|
},
|
|
593
655
|
timeoutMs: 30000,
|
|
@@ -606,7 +668,11 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
606
668
|
const lines = ['连接 ' + rec.connection + '(' + rec.engine + '):共 ' + tables.length + ' 张表' + (typeof rec.sizeBytes === 'number' && rec.sizeBytes >= 0 ? ',库体积 ' + formatBytes(rec.sizeBytes) : '') + '。'];
|
|
607
669
|
for (const item of tables.slice(0, 30)) {
|
|
608
670
|
const t = asRecord(item);
|
|
609
|
-
|
|
671
|
+
const label = typeof t.name === 'string' && t.name !== '' ? t.name : '(表清单读取失败)';
|
|
672
|
+
lines.push('- ' + label + ':' + (typeof t.rowCount === 'number' ? t.rowCount + ' 行' : '行数未知' + (t.error ? '(' + t.error + ')' : '')));
|
|
673
|
+
}
|
|
674
|
+
if (tables.length > 0 && tables.every((item) => typeof asRecord(item).rowCount !== 'number') && tables.every((item) => asRecord(item).error !== undefined)) {
|
|
675
|
+
lines.push('', '下一步:统计信息读不到时,先用 sql_schema 确认表清单可用、账号有读取系统表/information_schema 的权限,再用 sql_query 手动 COUNT(*)。');
|
|
610
676
|
}
|
|
611
677
|
if (tables.length > 30)
|
|
612
678
|
lines.push('…仅展示前 30 张表');
|
|
@@ -643,7 +709,7 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
643
709
|
}
|
|
644
710
|
}
|
|
645
711
|
else {
|
|
646
|
-
const names = await adapter.listTables(signal);
|
|
712
|
+
const names = await guard(signal, () => adapter.listTables(signal));
|
|
647
713
|
for (const table of names) {
|
|
648
714
|
try {
|
|
649
715
|
const result = await adapter.query('SELECT COUNT(*) FROM ' + quoteIdent(engine, table), 1, signal);
|
|
@@ -677,6 +743,15 @@ export function buildSqlTools(config, policyOf = () => 'unknown') {
|
|
|
677
743
|
lines.push('- 只读模式:' + (rec.readOnly === true ? '开(sql_exec 已禁用)' : '关'));
|
|
678
744
|
lines.push('- 写审批门:' + String(rec.writeApprovalDetail ?? (rec.writeApproval === true ? '开' : '关')));
|
|
679
745
|
lines.push('- 行数上限:' + String(rec.maxRows) + ';查询超时 ' + String(rec.queryTimeoutMs) + 'ms;写超时 ' + String(rec.execTimeoutMs) + 'ms');
|
|
746
|
+
// 自检的价值在于失败时告诉用户下一步做什么,而不只是把 ❌ 摆出来。
|
|
747
|
+
if (bad.length > 0) {
|
|
748
|
+
lines.push('- 下一步:逐个修好上面的 ❌ 连接(核对 host/port、数据库服务是否启动、账号密码或 SQLite 文件路径与权限),再运行 sql_health 复查。');
|
|
749
|
+
}
|
|
750
|
+
if (rec.writeApprovalReady === false) {
|
|
751
|
+
lines.push('- 下一步:写审批门当前无法确认。把会话审批策略切到可询问(ask),或由用户明确设置 writeApproval=false 后重启(之后 sql_exec 不再询问,慎用)。');
|
|
752
|
+
}
|
|
753
|
+
if (rec.readOnly === true)
|
|
754
|
+
lines.push('- 说明:readOnly=true 时 sql_exec 被禁用属于配置预期,不是故障。');
|
|
680
755
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
681
756
|
},
|
|
682
757
|
},
|
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.10","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.10",
|
|
4
4
|
"description": "DSH 数据库工具:SQLite / MySQL / PostgreSQL 查询、结构与统计、只读保护和写操作确认。欢迎提交 issues 和 PR。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"scripts": {
|
|
27
27
|
"build": "tsc -p tsconfig.json && node scripts/build-updates.mjs",
|
|
28
28
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
29
|
-
"test": "
|
|
30
|
-
"prepublishOnly": "
|
|
29
|
+
"test": "npm run build && node --test \"test/*.test.mjs\"",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
31
|
},
|
|
32
32
|
"dsh": {
|
|
33
33
|
"bundle": {
|