dsh-db-tool 1.7.2 → 1.8.0

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.
@@ -24,6 +24,70 @@ const SQL_MAINT_RE = /^\s*(shutdown|startup|archive\s+log|recover|analyze|optimi
24
24
  const SQL_HIDDEN_DANGER_RE = /[;]\s*\S|into\s+(outfile|dumpfile)|load_file\s*\(|set\s+(global|persist)|select\s+[\s\S]*\bfor\s+update\b/i;
25
25
  /** 读语句体内出现写关键字即 danger(WITH...DELETE / EXPLAIN ANALYZE DELETE 等) */
26
26
  const SQL_WRITE_BODY_RE = /\b(insert|update|delete|merge|drop|alter|truncate)\b/i;
27
+ /** SQL Server(T-SQL)专属高危首词。**仅 kind = 'sqlserver' 时启用**——通用 DDL/DML/MAINT
28
+ * 正则一律不动(既有 8 库判定口径保持不变)。
29
+ * T-SQL 把这些语句的首词写成通用正则之外(backup/restore/dbcc/reconfigure/bulk),若不追加
30
+ * 会在 execute 通道静默降级为 warning,确认门槛失效:
31
+ * - BACKUP / RESTORE:整库/日志备份与还原(可覆盖生产数据)
32
+ * - DBCC:修复/收缩/重建,部分选项会破坏数据页
33
+ * - RECONFIGURE / sp_configure:改实例级选项(含启用 xp_cmdshell、CLR、OLE 自动化)
34
+ * - BULK INSERT:从服务器可访问路径直接加载数据(绕应用层校验)
35
+ * - xp_cmdshell:扩展存储过程,可执行任意 OS 命令
36
+ * SHUTDOWN 已在通用 SQL_MAINT_RE 命中,此处列出只为给出 T-SQL 专属原因文案。 */
37
+ export const SQLSERVER_DANGER_HEADS = new Set([
38
+ 'backup', 'restore', 'dbcc', 'reconfigure', 'bulk', 'shutdown', 'sp_configure', 'xp_cmdshell',
39
+ ]);
40
+ /** T-SQL 语句体内出现即 danger 的形态:EXEC/EXECUTE 后接扩展存储过程 xp_*、sp_configure
41
+ * 或 sp_executesql(动态 SQL 入口)。覆盖 `EXEC xp_cmdshell`、`EXEC master..xp_cmdshell`、
42
+ * `EXEC @rc = master.dbo.xp_cmdshell`、`EXEC sp_configure 'show advanced options', 1`、
43
+ * `EXEC sp_executesql N'DROP TABLE t'`,以及远端字符串形态 `OPENQUERY(ls,'EXEC xp_...')`。
44
+ * 中间限长 200 字符,防跨语句误命中。
45
+ * **用「仅剥注释」的 body 判定,不用剥字面量的结构文本**:动态执行的证据就在字面量里,
46
+ * 剥掉就看不见了。代价是 `SELECT 'EXEC xp_cmdshell' AS s` 这类把该词写进字符串的读语句
47
+ * 也会要求确认(fail-closed,宁可多确认一次)。 */
48
+ const SQLSERVER_DANGER_BODY_RE = /\bexec(?:ute)?\b[\s\S]{0,200}?\b(?:xp_\w+|sp_configure|sp_executesql)\b/i;
49
+ /** T-SQL 动态 SQL 执行形态本身即 danger:EXEC/EXECUTE 后跟括号起始的字符串(`EXEC('…')`)、
50
+ * 字符串字面量(`EXEC N'…'` / `EXEC '…'`)或变量(`EXEC @sql` / `EXEC(@sql)`)——被执行的
51
+ * 文本在确认时不可知,静态清单无法判定其内容,故一律确认。
52
+ * 对照(不误报):`EXEC sp_who`、`EXEC dbo.usp_x`(静态对象名,走通用未识别 warning)。
53
+ * 同样用「仅剥注释」的 body 判定(需要看到括号与引号本身)。 */
54
+ const SQLSERVER_DYNAMIC_EXEC_RE = /\bexec(?:ute)?\s*(?:\(\s*)?(?:n\s*)?(?:'|@\w+)/i;
55
+ /** T-SQL 的 `SELECT ... INTO <表|#临时表>` 会创建对象(建表副作用),但通用
56
+ * SQL_HIDDEN_DANGER_RE 的 into 只认 MySQL 的 OUTFILE/DUMPFILE,不覆盖该形态。
57
+ * - 要求 select 在 into 之前(不误伤 INSERT INTO / MERGE ... USING);
58
+ * - `(?<!\bas\s)`:`SELECT 1 AS into FROM t` 的 into 是列别名,不是建表目标;
59
+ * - `(?!@)`:`INTO @t` 是表变量/变量赋值,不产生永久对象,放行(若日后要连表变量一起挡,
60
+ * 去掉该段即可,代价是 `SELECT 1 INTO @t` 这类常见写法每次都要确认)。
61
+ * 残留误报:`SELECT a FROM t ORDER BY into`(into 作未引用列名)会要求确认——fail-closed,
62
+ * 正常写法列名会加 [ ] 而结构文本已剥掉方括号标识符。 */
63
+ const SQLSERVER_SELECT_INTO_RE = /(?<!\bas\s)\binto\s+(?!@)/i;
64
+ /** 剥掉字符串字面量与带引号标识符,只留 SQL 结构:单引号串('' 转义,含 N 前缀)、
65
+ * 双引号标识符、方括号标识符(]] 转义)统一替换为空白。供 sqlserver 专属词法判定使用,
66
+ * 使 `SELECT 'into tbl'` / `SELECT [into] FROM t` 不被 INTO 规则误伤。 */
67
+ function stripSqlLiterals(statement) {
68
+ let out = '';
69
+ for (let i = 0; i < statement.length; i++) {
70
+ const ch = statement[i];
71
+ if (ch === "'" || ch === '"' || ch === '[') {
72
+ const close = ch === '[' ? ']' : ch;
73
+ let j = i + 1;
74
+ for (; j < statement.length; j++) {
75
+ if (statement[j] !== close)
76
+ continue;
77
+ if (statement[j + 1] === close) {
78
+ j++;
79
+ continue;
80
+ } // '' / "" / ]] 转义
81
+ break;
82
+ }
83
+ i = j; // 跳到闭合符(for 再 ++ 到其后)
84
+ out += ' ';
85
+ continue;
86
+ }
87
+ out += ch;
88
+ }
89
+ return out;
90
+ }
27
91
  /** MongoDB 读操作白名单(复用 mongodb 适配器导出的 READ_OPS,单一事实来源) */
28
92
  const MONGO_READ_OPS = READ_OPS;
29
93
  /** MongoDB 写操作清单(适配器未导出,此处按 DANGEROUS_OPS 之外的写命令复述;含命令文档形式 insert/update/delete/create) */
@@ -65,16 +129,46 @@ export function classifyStatement(kind, statement, op) {
65
129
  return classifyRedis(statement, op);
66
130
  if (kind === 'mongodb')
67
131
  return classifyMongo(statement, op);
68
- return classifySql(statement, op);
132
+ return classifySql(kind, statement, op);
69
133
  }
70
- /* ---------------- SQL 系(mysql/postgresql/gaussdb/sqlite/oracle/dmdb) ---------------- */
71
- function classifySql(statement, op) {
134
+ /* ---------------- SQL 系(mysql/postgresql/gaussdb/sqlite/oracle/dmdb/sqlserver) ---------------- */
135
+ function classifySql(kind, statement, op) {
72
136
  const head = sqlHead(statement);
73
137
  if (head === '')
74
138
  return { level: 'danger', reason: '无法解析语句关键字,按危险语句处理' };
75
139
  // 语句体分析一律基于剥注释后的文本:写词藏在注释里不应触发分级(误报),
76
140
  // 前导注释也不能使 DDL/DML 锚定失效(漏报——否则 /* x */ DROP 绕过确认)
77
141
  const body = stripSqlComments(statement);
142
+ // T-SQL 专属高危(BACKUP/RESTORE/DBCC/RECONFIGURE/sp_configure/xp_*/动态 SQL)先判:
143
+ // 这些形态不落在通用 DDL/DML/MAINT 正则上,不先判会降级为 warning(详见各常量注释)。
144
+ // 只对 sqlserver 生效,通用正则与其它 kind 的判定口径不受影响。
145
+ if (kind === 'sqlserver') {
146
+ // 1) 首词危险清单 与 动态执行证据(EXEC … xp_*/sp_configure/sp_executesql、EXEC('…')、
147
+ // EXEC @sql)都用「仅剥注释」的 body 判:动态执行的证据在字面量里,剥掉就看不见了。
148
+ if (SQLSERVER_DANGER_HEADS.has(head)
149
+ || SQLSERVER_DANGER_BODY_RE.test(body)
150
+ || SQLSERVER_DYNAMIC_EXEC_RE.test(body)) {
151
+ return { level: 'danger', reason: 'T-SQL 高危语句(备份/还原/DBCC/实例配置/扩展存储过程/动态 SQL),需要确认' };
152
+ }
153
+ // 2) 读头(select/with/show/set/…)就地判定并返回,把「写体扫描」换成剥字面量后的结构文本:
154
+ // `OPENQUERY(ls,'DELETE FROM t')` 的 delete 在远端字符串里,不是本语句的写操作(不升级);
155
+ // 而「多语句/未闭合字面量」仍用原文判——`SELECT 'a''; DROP TABLE t` 的第二条语句正是要拦的。
156
+ if (SQL_READ_HEADS.has(head)) {
157
+ if (SQL_HIDDEN_DANGER_RE.test(body)) {
158
+ return { level: 'danger', reason: '读语句包含危险片段(多语句 / 文件读写 / 全局设置 / 锁读),需要确认' };
159
+ }
160
+ const struct = stripSqlLiterals(body);
161
+ if (SQL_WRITE_BODY_RE.test(struct)) {
162
+ return { level: 'danger', reason: '读语句体内包含写操作关键字(如 WITH...DELETE / EXPLAIN 写语句),需要确认' };
163
+ }
164
+ // 3) SELECT ... INTO <表/临时表>:T-SQL 建表副作用(DDL 级),通用正则不覆盖(详见常量注释)。
165
+ // 已由上面写体扫描排除 WITH … INSERT INTO … SELECT(那是写操作,理由应指向写操作,不误挂这里)。
166
+ if ((head === 'select' || head === 'with') && SQLSERVER_SELECT_INTO_RE.test(struct)) {
167
+ return { level: 'danger', reason: 'T-SQL SELECT ... INTO 会创建目标表(DDL 副作用),需要确认' };
168
+ }
169
+ return { level: 'none' };
170
+ }
171
+ }
78
172
  const isRead = SQL_READ_HEADS.has(head);
79
173
  if (isRead) {
80
174
  if (SQL_HIDDEN_DANGER_RE.test(body)) {
@@ -56,7 +56,29 @@ export function redactUrl(url) {
56
56
  * 凭据按 encodeURIComponent 编码,只填一项只覆盖一项的语义与 setter 对齐;
57
57
  * 无 scheme://authority 形态的残缺串仍报「URL 无效」。 */
58
58
  export function mergeUrlCredentials(opts) {
59
- const { url, urlUser, urlPassword, secretsUrl } = opts;
59
+ const { urlUser, urlPassword, secretsUrl } = opts;
60
+ let url = opts.url;
61
+ // 前端编辑 URL 模式连接时回填的是脱敏值(safeUrl,含 :***@)。占位符不是凭据,必须先用
62
+ // 已存真实凭据替换它再走正常注入——host/port/path/query 保留用户的改动;否则会把字面量
63
+ // *** 当真密码落盘、连接静默失效(且 safeUrl 显示不变,用户看不出差别)。
64
+ // 非标准 URL 形态(jdbc: 前缀、多主机逗号端口等)无法可靠合并 → 明确报错要求重输。
65
+ if (url !== undefined && url.includes(':***@')) {
66
+ if (secretsUrl === undefined) {
67
+ throw new Error('URL 中含脱敏占位(***)且无已存真实 URL 可回退,请重新输入完整 URL 或清空用户名/密码框');
68
+ }
69
+ try {
70
+ const sec = new URL(secretsUrl);
71
+ const edit = new URL(url);
72
+ if (sec.host === '' || edit.host === '')
73
+ throw new Error('non-authority');
74
+ edit.username = sec.username;
75
+ edit.password = sec.password;
76
+ url = edit.toString();
77
+ }
78
+ catch {
79
+ throw new Error('连接 URL 含脱敏占位(***)且无法与已存凭据合并(非标准 URL 形态,如 jdbc: 前缀或多主机串):请重新输入完整 URL');
80
+ }
81
+ }
60
82
  if (urlUser === undefined && urlPassword === undefined)
61
83
  return url;
62
84
  let base;
@@ -65,12 +87,6 @@ export function mergeUrlCredentials(opts) {
65
87
  return undefined; // 无 URL 可注入,凭据忽略
66
88
  base = secretsUrl;
67
89
  }
68
- else if (url.includes(':***@')) {
69
- if (secretsUrl === undefined) {
70
- throw new Error('URL 中含脱敏占位(***)且无已存真实 URL 可回退,请重新输入完整 URL 或清空用户名/密码框');
71
- }
72
- base = secretsUrl;
73
- }
74
90
  else {
75
91
  base = url;
76
92
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "1.7.2",
3
+ "version": "1.8.0",
4
4
  "description": "DSH community plugin: chat-operated multi-database admin tool with sidebar management, project-scoped grants, ro/rw modes and dangerous-operation confirmation",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -26,8 +26,9 @@
26
26
  "test": "vitest run",
27
27
  "test:watch": "vitest",
28
28
  "typecheck": "tsc --noEmit",
29
- "build": "tsc -p tsconfig.build.json && node -e \"const fs=require('fs'); fs.mkdirSync('dist/script',{recursive:true}); fs.copyFileSync('lib/script/worker.cjs','dist/script/worker.cjs'); console.log('build OK')\" && node scripts/build-mongodb.mjs && node scripts/build-codemirror.mjs",
29
+ "build": "tsc -p tsconfig.build.json && node -e \"const fs=require('fs'); fs.mkdirSync('dist/script',{recursive:true}); fs.copyFileSync('lib/script/worker.cjs','dist/script/worker.cjs'); console.log('build OK')\" && node scripts/build-mongodb.mjs && node scripts/build-sqlserver.mjs && node scripts/build-codemirror.mjs",
30
30
  "build:mongodb": "node scripts/build-mongodb.mjs",
31
+ "build:sqlserver": "node scripts/build-sqlserver.mjs",
31
32
  "build:codemirror": "node scripts/build-codemirror.mjs",
32
33
  "prepack": "npm run build",
33
34
  "patch:dsh": "powershell -NoProfile -ExecutionPolicy Bypass -File scripts/apply-dsh-hotfix.ps1",
@@ -72,6 +73,7 @@
72
73
  "dsh-better-sidebar": "^0.24.1",
73
74
  "esbuild": "^0.28.0",
74
75
  "mongodb": "^7.6.0",
76
+ "mssql": "12.7.4",
75
77
  "sql-formatter": "^15.9.0",
76
78
  "stryker-cli": "^1.1.0",
77
79
  "typescript": "^5.9.3",
@@ -90,6 +92,7 @@
90
92
  "patches",
91
93
  "scripts",
92
94
  "vendor/mongodb-driver.cjs",
95
+ "vendor/sqlserver-driver.cjs",
93
96
  "vendor/codemirror-sql.cjs",
94
97
  "README.md",
95
98
  "README_EN.md"
@@ -46,7 +46,7 @@ import { EditorView, drawSelection, keymap } from '@codemirror/view';
46
46
  import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
47
47
  import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
48
48
  import { autocompletion } from '@codemirror/autocomplete';
49
- import { MySQL, PLSQL, PostgreSQL, SQLite, sql } from '@codemirror/lang-sql';
49
+ import { MySQL, MSSQL, PLSQL, PostgreSQL, SQLite, sql } from '@codemirror/lang-sql';
50
50
  import { tags } from '@lezer/highlight';
51
51
  import {
52
52
  formatDialect,
@@ -54,9 +54,10 @@ import {
54
54
  plsql as fmtPlsql,
55
55
  postgresql as fmtPostgresql,
56
56
  sqlite as fmtSqlite,
57
+ transactsql as fmtTransactsql,
57
58
  } from 'sql-formatter';
58
59
 
59
- // kind → lang-sql 内置方言(官方清单)。gaussdb 按 PG 方言、oracle/dmdb 按 PL/SQL。
60
+ // kind → lang-sql 内置方言(官方清单)。gaussdb 按 PG 方言、oracle/dmdb 按 PL/SQL、sqlserver 按 T-SQL。
60
61
  const SQL_DIALECTS = {
61
62
  mysql: MySQL,
62
63
  postgresql: PostgreSQL,
@@ -64,6 +65,7 @@ const SQL_DIALECTS = {
64
65
  sqlite: SQLite,
65
66
  oracle: PLSQL,
66
67
  dmdb: PLSQL,
68
+ sqlserver: MSSQL,
67
69
  };
68
70
 
69
71
  // kind → sql-formatter 方言对象。mongodb/redis 无 SQL 方言,format 原文返回。
@@ -74,6 +76,7 @@ const FMT_DIALECTS = {
74
76
  sqlite: fmtSqlite,
75
77
  oracle: fmtPlsql,
76
78
  dmdb: fmtPlsql,
79
+ sqlserver: fmtTransactsql,
77
80
  };
78
81
 
79
82
  // redis 常见命令表(大写原样补全)
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * 构建 vendor/sqlserver-driver.cjs:把 mssql(含 tedious)驱动 bundle 成单文件。
4
+ *
5
+ * 背景与 mongodb 同因(见 scripts/build-mongodb.mjs 头注释):DSH 宿主
6
+ * @deepseek-ai/dsh-app-boot(<= 0.1.7-rc.2)加载插件时对依赖树逐包调
7
+ * `require.resolve.paths(name)` 且不防护 null → 核心模块同名包会让整个插件无法加载。
8
+ * mssql → tedious → bl → readable-stream 链会引入 buffer / events / process /
9
+ * string_decoder 四个核心模块同名包,因此 mssql 只作 devDependency(提供 bundle 源与
10
+ * 类型),生产依赖树里不出现它。
11
+ *
12
+ * 不能把 @azure/identity、@azure/core-auth external 掉:tedious/lib/connection.js 顶部是
13
+ * 静态 require(非惰性),缺失会直接 MODULE_NOT_FOUND,故一并内联。
14
+ * msnodesqlv8 是 Windows 集成认证的独立入口(mssql/msnodesqlv8.js),不在主入口依赖链上,
15
+ * 保留为 external 以防驱动内部试探性 require。
16
+ *
17
+ * 每次执行全量重建(esbuild 秒级),不做幂等跳过,避免「源升级产物旧」。
18
+ */
19
+ import { build } from 'esbuild';
20
+ import { mkdirSync, statSync } from 'node:fs';
21
+ import path from 'node:path';
22
+ import { fileURLToPath } from 'node:url';
23
+
24
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
25
+ const outfile = path.join(root, 'vendor', 'sqlserver-driver.cjs');
26
+
27
+ /** msnodesqlv8 为可选原生驱动(本项目不支持 Windows 集成认证),不需要内联 */
28
+ const externals = ['msnodesqlv8'];
29
+
30
+ const entry = path.join(root, 'node_modules', 'mssql', 'index.js');
31
+ if (!statSync(entry, { throwIfNoEntry: false })) {
32
+ console.error('[build-sqlserver] 未找到 node_modules/mssql/index.js —— 请先 npm install(mssql 在 devDependencies,提供 bundle 源)');
33
+ process.exit(1);
34
+ }
35
+
36
+ mkdirSync(path.dirname(outfile), { recursive: true });
37
+ await build({
38
+ entryPoints: [entry],
39
+ outfile,
40
+ bundle: true,
41
+ platform: 'node',
42
+ format: 'cjs',
43
+ target: 'node20',
44
+ external: externals,
45
+ legalComments: 'none',
46
+ sourcemap: false,
47
+ minify: false,
48
+ logLevel: 'warning',
49
+ });
50
+
51
+ const size = statSync(outfile).size;
52
+ console.log(`[build-sqlserver] wrote vendor/sqlserver-driver.cjs (${(size / 1024 / 1024).toFixed(2)} MB)`);
53
+ console.log('[build-sqlserver] done');
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: db-admin
3
- description: 通过 dsh-db-tool 插件操作 8 种数据库(MySQL、PostgreSQL、GaussDB、SQLite、Redis、MongoDB、Oracle、达梦)。调用 DatabaseManager 工具完成连接查询、SQL/命令执行、库表浏览、数据预览;处理 NEEDS_CONFIRMATION 危险操作确认流程,遵守 ro/rw 项目授权与安全规范。
3
+ description: 通过 dsh-db-tool 插件操作 9 种数据库(MySQL、PostgreSQL、GaussDB、SQLite、Redis、MongoDB、Oracle、达梦、SQL Server)。调用 DatabaseManager 工具完成连接查询、SQL/命令执行、库表浏览、数据预览;处理 NEEDS_CONFIRMATION 危险操作确认流程,遵守 ro/rw 项目授权与安全规范。
4
4
  ---
5
5
 
6
6
  # db-admin:数据库管理与查询
@@ -33,7 +33,7 @@ DatabaseManager({ action: "query", conn_id: "c1", sql: "SELECT id, name FROM use
33
33
  ```
34
34
 
35
35
  - `sql` 必填;`params` 为参数数组(占位符见方言速查);可选 `challenge_id`(危险读如 Redis `KEYS *` 触发确认后重试用)。
36
- - 可选 `database`:跨库执行目标(Navicat 式)。pg/gaussdb 传库名(如 `gycwd`)即在该库上执行(同一凭据开独立连接,ro 授权下同样强制只读);其余库种忽略此参数(MySQL 可直接用 `库名.表` 语法)。
36
+ - 可选 `database`:跨库执行目标(Navicat 式)。pg/gaussdb 传库名(如 `gycwd`)即在该库上执行(同一凭据开独立连接,ro 授权下同样强制只读);其余库种优先用语句内全限定名(MySQL `库名.表`、SQL Server `库名.schema.表`)。
37
37
  - 返回 `{columns, rows, rowCount, truncated?}`,所有单元格已规范化为 string/number/null。
38
38
  - ro 与 rw 授权下都可用,但只读接口收到非查询语句会被拒。
39
39
 
@@ -93,7 +93,7 @@ DatabaseManager({ action: "run_script", conn_id: "mongo1", code: '{"find":"users
93
93
 
94
94
  这是这两种库唯一的执行入口(query/execute 不适用)。
95
95
 
96
- ## 八库方言速查
96
+ ## 九库方言速查
97
97
 
98
98
  | 库 | 占位符 | 元数据入口 | 分页 | 备注 |
99
99
  |---|---|---|---|---|
@@ -105,9 +105,17 @@ DatabaseManager({ action: "run_script", conn_id: "mongo1", code: '{"find":"users
105
105
  | MongoDB | 无(BSON 文档) | `listCollections` | `find().skip().limit()` | `run_script` 传 `{"find":"users","filter":{},"limit":20,"skip":40}` |
106
106
  | Oracle | `:name` | `ALL_TABLES` / `USER_TABLES` | `OFFSET m ROWS FETCH NEXT n ROWS ONLY` | |
107
107
  | 达梦(DM) | `:name` | 系统视图同 Oracle 风格 | `OFFSET m ROWS FETCH NEXT n ROWS ONLY`(8a 支持 `LIMIT`) | |
108
+ | SQL Server | `?`(适配器内部重写为 `@p1`) | `sys.*` catalog views | 2012+ `OFFSET m ROWS FETCH NEXT n ROWS ONLY`;2008/2008R2 降级 `ROW_NUMBER` | 标识符 `[x]`,名中含 `]` 双写为 `]]`;**仅 SQL 认证**(不支持 Windows 集成认证 / Azure AD);其余边界见下方「SQL Server 覆盖范围与边界」 |
108
109
 
109
110
  **类型规范化**(工具返回值已自动处理,引用结果时注意):`DECIMAL/NUMERIC` → string(避免精度丢失);`Date/TIMESTAMP` → ISO 8601 string;`LOB/CLOB/BLOB` → string;MongoDB `ObjectId` → string。生成写语句时,DECIMAL 直接传字符串值、日期传 ISO 字符串即可。
110
111
 
112
+ **SQL Server 类型例外(分侧口径)**:
113
+ - **decimal/numeric**:浏览侧(`query` / `preview` / 数据浏览网格)**不保证逐位保真**——驱动在解析阶段已把它转成 JS number(>15 有效位即丢精度),转成字符串只能防后续数值化,且 >1e21 会显示为科学计数法;需要逐位保真时请在 SQL 里显式 `CONVERT(nvarchar(...), col)`。数据传输(transfer)读端已内建 `CONVERT(nvarchar(64), col)`,写端按字符串绑定,精度不经过 JS 浮点。
114
+ - **空间/层次 UDT**:`hierarchyid` / `geometry` / `geography` 在传输中按二进制(bytes)搬运,目标端建 `VARBINARY(MAX)`——值保真但 **UDT 类型语义不保留**(目标端没有 `ST_*` / `hierarchyid` 方法),需要空间语义时目标端要自行改造列类型。
115
+ - **sql_variant**:传输读端 `CONVERT(nvarchar(max), col)` 转文本,**原类型信息丢失**(驱动按值的实际类型返回标量,无法统一搬运)。
116
+ - **字符列保容量**:源 `varchar` / `char` 长度 4001–8000 会映射为目标 `NVARCHAR(MAX)` 类,不做隐式收窄。
117
+ - **日期时间保真**:`datetime2` 读端 `CONVERT(nvarchar(33), col, 126)`、`time` 用 `CONVERT(nvarchar(16), col, 126)`。
118
+
111
119
  ## GaussDB 官方对齐注记(HCS 26.861.0,集中式/分布式)
112
120
 
113
121
  依据华为云 GaussDB 官方文档(doc.hcs.huawei.com/db/zh-cn,26.861.0 双形态)与 openGauss 官方文档源仓库对齐;未真机验证处以代码内标注为准。
@@ -120,6 +128,17 @@ DatabaseManager({ action: "run_script", conn_id: "mongo1", code: '{"find":"users
120
128
  - **兼容模式**:openGauss 有 sql_compatibility(A/B/C/PG)兼容模式,部分语法语义随模式变化(如 B 模式 SET SESSION TRANSACTION 需 `b_format_behavior_compat_options` 含 set_session_transaction 才生效),本工具按 PG 语义使用。
121
129
  - 驱动/默认值差异的代码内证据与证据 URL 见 `lib/adapters/gaussdb/index.ts` 头注释与 `lib/adapters/sql-shared/pg-like.ts`。
122
130
 
131
+ ## SQL Server 覆盖范围与边界
132
+
133
+ - 覆盖 SQL Server 2008/2008R2 → 2022 与 Azure SQL Database;SQL Server 2005(9.x)及更早版本不支持、未验证;`tdsVersion=8_0`(TDS 8.0)与 `encrypt='strict'`(严格加密)也不支持——SQL Server 2022 用默认 `7_4` 即可正常连接。方言与元数据只使用 2005+ 通用写法(元数据走 `sys.*` catalog views)。
134
+ - **认证**:仅 SQL 认证(user/password);**不支持 Windows 集成认证,也不支持 Azure AD 认证**——需要这两类认证时本工具不可用。
135
+ - **连接形态**:`sqlserver://user:password@host:port/db`,支持 query 参数 `encrypt` / `trustServerCertificate` / `tdsVersion` / `instanceName` / `useUTC`(参数名大小写不敏感);`tdsVersion` 默认 `7_4`,可配 `7_3_A` / `7_3_B` / `7_2` / `7_1`。**不接受** ADO.NET 键值连接串(`Server=...;Database=...`)。
136
+ 默认 `encrypt=true` + `trustServerCertificate=false`(同 mssql 官方默认);**连 SQL Server 2008/2008R2 通常要在 URL 追加 `?trustServerCertificate=true`**(旧实例多为自签名证书、可能只支持 TLS 1.0,Node 默认最低 TLSv1.2 会握手失败),服务器未强制加密时也可 `?encrypt=false`,并建议显式 `?tdsVersion=7_3_A`(2008)/ `7_3_B`(2008R2)。
137
+ - **分页自动分流**:适配器探测 `SERVERPROPERTY('ProductMajorVersion')`,2012+ 用 `OFFSET/FETCH`,2008/2008R2 降级 `ROW_NUMBER() OVER (ORDER BY (SELECT NULL))` 子查询——生成 SQL 时不需要自己补 `TOP`/`ROW_NUMBER`。
138
+ - **传输边界**:写入为参数化多行 `VALUES` INSERT(TDS 单语句 2100 参数上限、`VALUES` 单语句 1000 行上限,每语句行数取两者最小值)。传输分页按目标库版本分流:2012+ 用 `OFFSET/FETCH`,**2008/2008R2 降级可用**——有序游标(keyset/range)用 `SELECT TOP (n) ... ORDER BY key`,无分片键的 offset 全序与 `keyAt` 切块用 `ROW_NUMBER() OVER (...)` 子查询;版本探测失败按 0 走保守降级。已知限制:目标表已存在且该列是 IDENTITY 时,写入显式值会报错 544(当前不自动 `SET IDENTITY_INSERT ON`);传输自动建表不生成 IDENTITY 列。
139
+ - **验证状态**:本轮只交付离线单测(Fake 驱动),**未经真机验证**;真机冒烟开关 `DBT_TEST_SQLSERVER_URL`。
140
+ - 只读(ro)强度弱于 MySQL/PG,原因见下文「只读(ro)模式下的正确姿势」。
141
+
123
142
  ## 危险操作确认流程(NEEDS_CONFIRMATION)
124
143
 
125
144
  收到形如以下的返回时:
@@ -137,12 +156,12 @@ DatabaseManager({ action: "run_script", conn_id: "mongo1", code: '{"find":"users
137
156
 
138
157
  Challenge 属性:**一次性**(用过即失效)、绑定语句 hash(改语句后失效)、5 分钟过期。过期或无效会返回 `INVALID_CHALLENGE`,此时重新发起调用、走完整确认流程。**严禁**未经用户确认就重试;也**不要**为绕过确认而改写语句。
139
158
 
140
- 会触发确认的典型操作:DDL(CREATE/ALTER/DROP/TRUNCATE)、所有 DML 写(INSERT/UPDATE/DELETE)、维护命令、Redis 危险命令(FLUSHALL、CONFIG 等)与写命令、Mongo 写 op(drop、deleteMany、updateMany 等)、以及部分危险读(如 Redis `KEYS *`)。
159
+ 会触发确认的典型操作:DDL(CREATE/ALTER/DROP/TRUNCATE)、所有 DML 写(INSERT/UPDATE/DELETE)、维护命令、Redis 危险命令(FLUSHALL、CONFIG 等)与写命令、Mongo 写 op(drop、deleteMany、updateMany 等)、以及部分危险读(如 Redis `KEYS *`)。SQL Server 另有:专属危险首词 `BACKUP`(DATABASE/LOG)、`RESTORE`、`DBCC`、`RECONFIGURE`、`BULK INSERT`、`SHUTDOWN`、`xp_cmdshell` 与其他 `xp_*`、`sp_configure`;`SELECT ... INTO <表|#临时表>`(T-SQL 建表副作用)与 `WITH ... (DELETE|UPDATE|INSERT)` 也判危险需确认;其中 `SELECT 1 INTO @t`(表变量/变量赋值)放行、`INSERT INTO ...` 仍按 DML 口径。注意一般 `EXEC sp_*`(非 `sp_configure`/`xp_*`)**未**单列危险,不会因此触发确认。另外这一层是「确认」语义;**ro 授权下** `SELECT ... INTO`、`WITH` 写体与 `OPENQUERY`/`OPENROWSET`/`OPENDATASOURCE` 会被适配器闸门**直接拒绝**(返回错误,无确认可走)。
141
160
 
142
161
  ## 安全规范(必须遵守)
143
162
 
144
163
  - **永远使用参数绑定**:`sql: "SELECT * FROM users WHERE id = ?"` + `params: [42]`。**禁止字符串拼接**用户输入进 SQL。
145
- - **标识符白名单**:表名/列名无法参数化时,只能使用 `schema` action 返回的标识符,或引号包裹并转义(MySQL `` `name` ``、PG/SQLite `"name"`、Oracle/DM `"NAME"`);禁止把用户原文直接嵌为标识符。
164
+ - **标识符白名单**:表名/列名无法参数化时,只能使用 `schema` action 返回的标识符,或引号包裹并转义(MySQL `` `name` ``、PG/SQLite `"name"`、Oracle/DM `"NAME"`、SQL Server `[name]` 且名字中的 `]` 双写为 `]]`);禁止把用户原文直接嵌为标识符。
146
165
  - 值中的引号交给参数绑定处理;确实无法绑定的场景先向用户确认。
147
166
  - 不执行来历不明的语句;用户要求执行时,按危险确认流程处理。
148
167
 
@@ -151,3 +170,4 @@ Challenge 属性:**一次性**(用过即失效)、绑定语句 hash(改
151
170
  - ro 授权下 execute/写类 run_script 会返回 `READ_ONLY`——**不要反复重试**,直接告知用户需要在侧边栏把授权升级为 rw。
152
171
  - 探索数据用 `query` + `schema` + `preview` 组合即可完成绝大多数只读任务;批量导出优先分页(LIMIT/OFFSET 或 SCAN/fetch)而非一次性大查询。
153
172
  - Redis ro 下用 `SCAN` 而非 `KEYS`;Mongo ro 下只允许 find/count 类 op。
173
+ - **SQL Server 的 ro 强度弱于 MySQL/PG**:SQL Server 没有等价的会话级只读语句。ro 下适配器 query 闸门有五道:① 首词白名单(select/with/show/describe/desc/explain/use/set/help/table);② 拒绝多语句(词法级:顶层分号之后出现任何内容即拒);③ 拒绝 `SELECT ... INTO`(token 级,字符串/方括号标识符/注释内的 into 不误伤);④ 拒绝 `WITH ... (INSERT|UPDATE|DELETE|MERGE)`;⑤ 拒绝 `OPENQUERY`/`OPENROWSET`/`OPENDATASOURCE`(远端/外部数据源执行任意语句、无法审计,连合法只读远端查询也一律拒)。服务层另拦 execute,guard 层对同样两类「读头写体」还会要求确认。**不要把 ro 连接当成数据库侧的只读隔离**;需要强隔离请换数据库侧只读账号。