dsh-db-tool 1.7.2 → 1.8.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/README.md CHANGED
@@ -6,26 +6,45 @@ DSH 社区插件:在聊天中安全操作数据库,配套侧边栏管理台
6
6
 
7
7
  ## 功能
8
8
 
9
- - **8 种数据库**:MySQL、PostgreSQL、GaussDB、SQLite、Redis、MongoDB、Oracle、达梦(DM)
9
+ - **9 种数据库**:MySQL、PostgreSQL、GaussDB、SQLite、Redis、MongoDB、Oracle、达梦(DM)、SQL Server
10
10
  - **DatabaseManager 单工具多 action**:`list_connections / query / execute / schema / preview / run_script`(`run_script` 在 node:vm 沙箱中执行,60s 超时,仅注入受限 `db.{query,execute}` 句柄)
11
11
  - **分级权限**:连接级只读(ro)/读写(rw)+ 项目级授权(`grants.json`:projectPathKey → 连接 → 模式);未授权项目一律拒绝
12
12
  - **危险操作确认**:DDL / FLUSHALL / dropDatabase 等先返回 `NEEDS_CONFIRMATION`,对话内(模型经 ask)或 SQL 控制台(弹窗)确认后携一次性 `challengeId`(绑定语句 SHA256、5 分钟过期)重试
13
13
  - **审计**:全部执行落 `audit.jsonl`(语句、危险级、是否确认、结果)
14
14
  - **侧边栏 4 面板**(dsh-better-sidebar,zh/en):连接管理、项目授权、数据浏览、SQL 控制台
15
- - **db-admin skill**:随插件分发,覆盖 8 库方言速查、安全规范、确认流程
15
+ - **db-admin skill**:随插件分发,覆盖 9 库方言速查、安全规范、确认流程
16
16
 
17
17
  ### SQL 控制台
18
18
 
19
19
  侧边栏 SQL 控制台面板已升级为 Navicat 式多标签编辑器(CodeMirror 6):
20
20
 
21
21
  - **多标签**:新建/关闭/切换查询标签,每个标签独立保存 SQL、结果与历史(历史上限 50 条,可展开点击回填)
22
- - **语法高亮与补全**:按方言切换——mysql→MySQL、postgresql/gaussdb→PostgreSQL、sqlite→SQLite、oracle/dmdb→PL/SQL;Redis/Mongo 为关键字/方法补全的纯文本编辑;表列元数据自动拉取注入补全(上限 50 表)。编辑器 bundle(`vendor/codemirror-sql.cjs`,`npm run build:codemirror` 生成,`npm run build` 自动串接)加载失败时静默降级 textarea
22
+ - **语法高亮与补全**:按方言切换——mysql→MySQL、postgresql/gaussdb→PostgreSQL、sqlite→SQLite、oracle/dmdb→PL/SQL、sqlserver→T-SQL(`@codemirror/lang-sql` 的 MSSQL 方言);Redis/Mongo 为关键字/方法补全的纯文本编辑;表列元数据自动拉取注入补全(上限 50 表)。编辑器 bundle(`vendor/codemirror-sql.cjs`,`npm run build:codemirror` 生成,`npm run build` 自动串接)加载失败时静默降级 textarea
23
23
  - **快捷键**:`Ctrl/Cmd+Enter` 执行全部,`Ctrl/Cmd+Shift+Enter` 执行选中
24
- - **格式化**:sql-formatter 按方言格式化,解析失败回退原文
24
+ - **格式化**:sql-formatter 按方言格式化(sqlserver→`transactsql`),解析失败回退原文
25
25
  - **逐条执行**:客户端预分类读/写语句分别路由 `/api/query` 与 `/api/execute`,只读授权下写语句自动跳过并标注;出错可一键定位到对应语句起始处
26
+ - **批分隔符 GO**:支持 `GO` 与 `GO n`——按 sqlcmd 语义把上一批重复执行 n 次(`GO 0` / `GO 1` 均等同 1 次),`GO` 行本身不产生语句;重复计数上限 **1000**,超出按 1000 截断,防止一次执行被放大成天量请求
26
27
  - **事务**:rw 授权下可开始/提交/回滚(服务端粘性连接会话;空闲 5 分钟超时自动回滚并审计,授权回收/降级即时生效)
27
28
  - **结果网格**:表头三态排序、50 行/页分页、单元格点击复制、CSV/JSON 导出
28
29
 
30
+ ## SQL Server 支持范围与边界
31
+
32
+ 覆盖 SQL Server 2008/2008R2 → 2022 与 Azure SQL Database;方言与元数据只使用 SQL Server 2005+ 的通用写法。
33
+
34
+ - **连接形态**:唯一形态为 `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`;端口默认 1433,指定 `instanceName` 时按 tedious 语义忽略端口、走 SQL Browser(UDP 1434);最低 TLS 版本经 `cryptoCredentialsDetails.minVersion` 配置,`ssl=true` 等价于 `encrypt=true`。**不接受** ADO.NET 键值连接串(`Server=...;Database=...`)。
35
+ 默认 `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)。
36
+ - **认证边界**:仅 SQL 认证(`user` / `password`)。**不支持 Windows 集成认证**,**不支持 Azure AD 认证**。
37
+ - **方言**:参数占位符对外只写 `?`,适配器内部按顺序重写为 `@p1..@pN`;标识符用 `[x]`,名字中的 `]` 双写为 `]]`;库/表/列元数据只走 `sys.*` catalog views(`sys.databases` / `sys.schemas` / `sys.objects` / `sys.columns` / `sys.types` / `sys.indexes` / `sys.extended_properties`),只用 2005+ 通用写法。
38
+ - **分页按版本分流**:连接后查 `SERVERPROPERTY('ProductMajorVersion')`(取不到时回退解析 `ProductVersion` 前缀)并缓存;major ≥ 11(2012+,含 Azure SQL 的 12)用 `ORDER BY (SELECT NULL) OFFSET n ROWS FETCH NEXT m ROWS ONLY`,< 11(2008/2008R2)降级 `ROW_NUMBER() OVER (ORDER BY (SELECT NULL))` 子查询;探测失败同样走降级分支。
39
+ - **decimal 精度分侧口径**:浏览侧(`query` / `preview` / 数据浏览网格)**不保证 decimal/numeric 逐位保真**——tedious 在解析阶段已把 decimal 转成 JS number(>15 有效位即丢精度),适配器转字符串只能防后续数值化,且 >1e21 会出现科学计数法;需要逐位保真时请在 SQL 里显式 `CONVERT(nvarchar(...), col)`。数据传输(transfer)读端已内建该转换(`CONVERT(nvarchar(64), col)`),写入时按字符串绑定,精度不经过 JS 浮点。
40
+ - **类型搬运口径(传输侧)**:`hierarchyid` / `geometry` / `geography` 按二进制(bytes)保真搬运,目标端建 `VARBINARY(MAX)`——**UDT 类型语义不保留**(目标端没有 `ST_*` / `hierarchyid` 方法);`sql_variant` 读端 `CONVERT(nvarchar(max), col)` 转文本,**原类型信息丢失**(驱动按值的实际类型返回标量,无法统一搬运);`text` / `ntext` / `xml` → `NVARCHAR(MAX)`,源 `varchar` / `char` 长度 4001–8000 也映射为 `NVARCHAR(MAX)` 类(保容量,不做隐式收窄),`varbinary` / `image` / `rowversion` → `VARBINARY(MAX)`,`decimal` / `numeric` / `money` → `DECIMAL`;`datetime2` 读端 `CONVERT(nvarchar(33), col, 126)`、`time` 用 `CONVERT(nvarchar(16), col, 126)` 保真。
41
+ - **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 层对同样两类「读头写体」还会要求确认。**这仍是应用层拦截,不等同于数据库侧的只读会话**;需要强隔离时请给连接配数据库侧只读账号。
42
+ - **数据传输**:写入走参数化多行 `VALUES` INSERT(TDS 单语句参数上限 2100、`VALUES` 表值构造器单语句最多 1000 行,每语句行数取两者最小值;replace/ignore 走 `MERGE`)。传输分页同样按目标库版本分流:2012+ 用 `OFFSET/FETCH`,2008/2008R2 降级——有序游标(keyset/range)用 `SELECT TOP (n) ... ORDER BY key`,无分片键的 offset 全序与 `keyAt` 切块用 `ROW_NUMBER() OVER (...)` 子查询;版本探测失败时按 0 走保守降级,因此**传输在 SQL Server 2008/2008R2 同样可用**。已知限制:目标表已存在且该列为 IDENTITY 时,写入显式值会报错 544(当前不自动 `SET IDENTITY_INSERT ON`);传输自动建表不生成 IDENTITY 列。建表 DDL 在显式事务中可回滚,不需要 Oracle/DM 那样的隐式提交处理。
43
+ - **验证状态**:本轮只交付离线单测(Fake 驱动),SQL Server 路径**未经真机验证**;真机冒烟开关 `DBT_TEST_SQLSERVER_URL`(形如 `sqlserver://sa:pw@host:1433/db`)。
44
+ - **驱动形态**:`mssql@12` 仅在 devDependencies,运行时加载 `npm run build:sqlserver` 生成的 `vendor/sqlserver-driver.cjs`;原因是 mssql → tedious → bl → readable-stream 会把 buffer/events/process/string_decoder 带进生产依赖树,触发宿主 `dsh-app-boot` 的 `require.resolve.paths` bug,导致插件整体加载失败。
45
+ - **错误消息脱敏**:连接 URL 解析失败时,错误文案里的 userinfo 已脱敏,不回显明文密码(错误会进 `audit.jsonl` 与模型上下文,故必须脱敏)。
46
+ - **不支持**:SQL Server 2005(9.x)及更早版本,未支持也未验证;`tdsVersion=8_0`(TDS 8.0)与 `encrypt='strict'`(严格加密)亦未支持——SQL Server 2022 用默认 `7_4` 即可正常连接,TDS 8.0 强加密属后续增强。
47
+
29
48
  ## 数据布局
30
49
 
31
50
  `$DSH_HOME/db-tool/`(0700):
@@ -39,7 +58,7 @@ DSH 社区插件:在聊天中安全操作数据库,配套侧边栏管理台
39
58
 
40
59
  ## 安装
41
60
 
42
- 8 种数据库驱动全部随 npm 包分发(SQLite 优先用 Node 内置 `node:sqlite`,可选 `better-sqlite3`;GaussDB 驱动为华为云官方 npm 包 `gaussdb-node`),无需任何构建步骤。GaussDB 认证支持 sha256 与 md5,md5-sha256 混合与 SM3 暂不支持。
61
+ 9 种数据库驱动随 npm 包分发(SQLite 优先用 Node 内置 `node:sqlite`,可选 `better-sqlite3`;GaussDB 驱动为华为云官方 npm 包 `gaussdb-node`),无需任何构建步骤。其中 SQL Server 驱动(`mssql@12`)只在 devDependencies,运行时加载发布包内置的 `vendor/sqlserver-driver.cjs`(`npm run build:sqlserver` 生成)——mssql 的生产依赖树会带入 buffer/events/process/string_decoder,触发宿主 resolve.paths bug。GaussDB 认证支持 sha256 与 md5,md5-sha256 混合与 SM3 暂不支持。
43
62
 
44
63
  唯一的一次性交互来自 oracledb:其 install 脚本会被 pnpm 默认拦截并使首次安装报告失败——在 DSH 插件安装界面点 **"Allow these scripts and retry"** 即可完成安装(该脚本仅做 Node 版本检查与横幅打印,批准无风险;批准持久化到 profile,后续升级不再提示)。
45
64
 
@@ -78,19 +97,19 @@ npx tsc --noEmit
78
97
  npx stryker run # 变异测试(范围 lib/guard + lib/manager + lib/store,报告 reports/mutation/)
79
98
  ```
80
99
 
81
- 真机冒烟(设了才跑):`DBT_TEST_MYSQL_URL / DBT_TEST_PG_URL / DBT_TEST_REDIS_URL / DBT_TEST_DM_CONNECT / DBT_TEST_MONGO_URL / DBT_TEST_ORACLE_CONNECT`。GaussDB 与 Oracle/Mongo 官方要求均按官方文档实现,未真机验证处以代码内标注为准。
100
+ 真机冒烟(设了才跑):`DBT_TEST_MYSQL_URL / DBT_TEST_PG_URL / DBT_TEST_REDIS_URL / DBT_TEST_DM_CONNECT / DBT_TEST_MONGO_URL / DBT_TEST_ORACLE_CONNECT / DBT_TEST_SQLSERVER_URL`(SQL Server 形如 `sqlserver://sa:pw@host:1433/db`)。GaussDB、Oracle/Mongo 与 SQL Server 的要求均按官方文档实现,未真机验证处以代码内标注为准;**本轮 SQL Server 只有离线单测(Fake 驱动),未经真机验证**。
82
101
 
83
102
  ## 目录结构
84
103
 
85
104
  ```
86
- lib/ host 插件(store / adapters×8 / guard / manager / http / index)
105
+ lib/ host 插件(store / adapters×9 / guard / manager / http / index)
87
106
  client/ 侧边栏单文件产物(client.js,即源码)
88
107
  skills/ db-admin skill
89
- scripts/ 构建与工具脚本(mongodb 驱动 / CodeMirror 编辑器 bundle、DSH 宿主热修复 patch:dsh 等)
108
+ scripts/ 构建与工具脚本(mongodb / sqlserver 驱动、CodeMirror 编辑器 bundle、DSH 宿主热修复 patch:dsh 等)
90
109
  patches/ DSH 宿主 dsh-app-boot 热修复补丁(patch:dsh 按宿主版本选用)
91
110
  docs/ 预留(当前为空)
92
111
  tests/ vitest(离线 mock + DBT_TEST_* 门控真机)
93
- vendor/ mongodb-driver.cjs 与 codemirror-sql.cjs bundle(gitignore,发布经 files 白名单收录)
112
+ vendor/ mongodb-driver.cjs、sqlserver-driver.cjs 与 codemirror-sql.cjs bundle(gitignore,发布经 files 白名单收录)
94
113
  ```
95
114
 
96
115
  ## 许可证
package/README_EN.md CHANGED
@@ -6,26 +6,45 @@ A DSH community plugin for operating databases safely from chat, with a sidebar
6
6
 
7
7
  ## Features
8
8
 
9
- - **8 databases**: MySQL, PostgreSQL, GaussDB, SQLite, Redis, MongoDB, Oracle, and DM (Dameng)
9
+ - **9 databases**: MySQL, PostgreSQL, GaussDB, SQLite, Redis, MongoDB, Oracle, DM (Dameng), and SQL Server
10
10
  - **One tool, six actions**: `DatabaseManager` exposes `list_connections / query / execute / schema / preview / run_script` (`run_script` executes in a `node:vm` sandbox with a 60s timeout and only restricted `db.{query,execute}` handles)
11
11
  - **Tiered permissions**: per-connection read-only (ro) / read-write (rw) plus project-scoped grants (`grants.json`: projectPathKey → connection → mode); unauthorized projects are always rejected
12
12
  - **Dangerous-operation confirmation**: DDL / FLUSHALL / dropDatabase and the like first return `NEEDS_CONFIRMATION`; after confirmation in chat (model via ask) or in the SQL console (dialog), the retry carries a one-time `challengeId` (bound to the statement SHA256, 5-minute TTL)
13
13
  - **Auditing**: every execution is appended to `audit.jsonl` (statement, danger level, confirmed or not, outcome)
14
14
  - **Sidebar with 4 panels** (dsh-better-sidebar, zh/en): connections, project grants, data browser, SQL console
15
- - **db-admin skill**: shipped with the plugin, covering dialect cheat sheets for all 8 databases, safety rules, and the confirmation flow
15
+ - **db-admin skill**: shipped with the plugin, covering dialect cheat sheets for all 9 databases, safety rules, and the confirmation flow
16
16
 
17
17
  ### SQL Console
18
18
 
19
19
  The sidebar SQL console panel has been upgraded to a Navicat-style multi-tab editor (CodeMirror 6):
20
20
 
21
21
  - **Multi-tab**: create/close/switch query tabs; each tab keeps its own SQL, result, and history (capped at 50 entries, expandable and click-to-refill)
22
- - **Syntax highlighting & completion**: switches with the dialect — mysql→MySQL, postgresql/gaussdb→PostgreSQL, sqlite→SQLite, oracle/dmdb→PL/SQL; Redis/Mongo get keyword/method completion over plain text; table/column metadata is fetched to feed completion (up to 50 tables). If the editor bundle (`vendor/codemirror-sql.cjs`, built by `npm run build:codemirror`, chained after `npm run build`) fails to load, it silently falls back to a textarea
22
+ - **Syntax highlighting & completion**: switches with the dialect — mysql→MySQL, postgresql/gaussdb→PostgreSQL, sqlite→SQLite, oracle/dmdb→PL/SQL, sqlserver→T-SQL (the MSSQL dialect from `@codemirror/lang-sql`); Redis/Mongo get keyword/method completion over plain text; table/column metadata is fetched to feed completion (up to 50 tables). If the editor bundle (`vendor/codemirror-sql.cjs`, built by `npm run build:codemirror`, chained after `npm run build`) fails to load, it silently falls back to a textarea
23
23
  - **Shortcuts**: `Ctrl/Cmd+Enter` runs everything, `Ctrl/Cmd+Shift+Enter` runs the selection
24
- - **Formatting**: dialect-aware formatting via sql-formatter, falling back to the original text on parse failure
24
+ - **Formatting**: dialect-aware formatting via sql-formatter (sqlserver→`transactsql`), falling back to the original text on parse failure
25
25
  - **Per-statement execution**: statements are pre-classified client-side as read/write and routed to `/api/query` vs `/api/execute`; write statements are skipped and flagged under read-only grants; errors can jump to the offending statement
26
+ - **`GO` batch separator**: `GO` and `GO n` follow sqlcmd semantics — the preceding batch is repeated n times (`GO 0` / `GO 1` both mean once) and the `GO` line itself produces no statement; the repeat count is capped at **1000** (values above it are truncated) so one run cannot be amplified into an unbounded number of requests
26
27
  - **Transactions**: begin/commit/rollback under rw grants (server-side sticky-connection session; idle sessions roll back after a 5-minute TTL and are audited; grant revocation takes effect immediately)
27
28
  - **Result grid**: tri-state column sorting, pagination at 50 rows/page, click-to-copy cells, CSV/JSON export
28
29
 
30
+ ## SQL Server Support Scope and Boundaries
31
+
32
+ Coverage: SQL Server 2008/2008R2 → 2022 and Azure SQL Database; the dialect and metadata queries only use SQL Server 2005+ constructs.
33
+
34
+ - **Connection form**: exclusively `sqlserver://user:password@host:port/db`, with query parameters `encrypt` / `trustServerCertificate` / `tdsVersion` / `instanceName` / `useUTC` (names are case-insensitive). `tdsVersion` defaults to `7_4` and accepts `7_3_A` / `7_3_B` / `7_2` / `7_1`; the port defaults to 1433, and when `instanceName` is set the port is ignored per tedious semantics (SQL Browser over UDP 1434); the minimum TLS version is set via `cryptoCredentialsDetails.minVersion`, and `ssl=true` is equivalent to `encrypt=true`. ADO.NET key/value connection strings (`Server=...;Database=...`) are **not** accepted.
35
+ The defaults are `encrypt=true` + `trustServerCertificate=false` (the same as mssql's own defaults). **Connecting to SQL Server 2008/2008R2 usually requires appending `?trustServerCertificate=true`**: those instances typically use self-signed certificates and may only speak TLS 1.0, while Node's default minimum is TLSv1.2, so the defaults fail the handshake; use `?encrypt=false` when the server does not force encryption, and set `?tdsVersion=7_3_A` (2008) / `7_3_B` (2008R2) explicitly.
36
+ - **Authentication**: SQL authentication only (`user` / `password`). **Windows integrated authentication is not supported**, and neither is **Azure AD authentication**.
37
+ - **Dialect**: write `?` as the parameter placeholder — the adapter rewrites them in order to `@p1..@pN`; identifiers use `[x]` with `]` doubled as `]]`; database/table/column metadata goes through `sys.*` catalog views only (`sys.databases` / `sys.schemas` / `sys.objects` / `sys.columns` / `sys.types` / `sys.indexes` / `sys.extended_properties`), using 2005+ constructs only.
38
+ - **Paging is version-split**: after connecting it reads `SERVERPROPERTY('ProductMajorVersion')` (falling back to parsing the `ProductVersion` prefix) and caches the result; major ≥ 11 (2012+, including Azure SQL's 12) uses `ORDER BY (SELECT NULL) OFFSET n ROWS FETCH NEXT m ROWS ONLY`, while < 11 (2008/2008R2) degrades to a `ROW_NUMBER() OVER (ORDER BY (SELECT NULL))` subquery; a failed probe also takes the degraded branch.
39
+ - **decimal precision is side-dependent**: on the browsing side (`query` / `preview` / data-browser grid) **decimal/numeric is not guaranteed digit-exact** — tedious has already turned decimal into a JS number during parsing (>15 significant digits lose precision), and converting it to a string afterwards only prevents later numeric coercion (>1e21 also renders in scientific notation); ask for `CONVERT(nvarchar(...), col)` in SQL when you need digit-exact values. The transfer reader already does that conversion (`CONVERT(nvarchar(64), col)`) and binds strings on write, so precision never passes through JS floats.
40
+ - **Type mapping on the transfer side**: `hierarchyid` / `geometry` / `geography` move as binary (bytes) into `VARBINARY(MAX)` on the target — the values are preserved but **UDT type semantics are not** (no `ST_*` / `hierarchyid` methods on the target); `sql_variant` is read as `CONVERT(nvarchar(max), col)` text, so **the original type information is lost** (the driver returns scalars of the value's actual type, which cannot be moved uniformly); `text` / `ntext` / `xml` → `NVARCHAR(MAX)`, source `varchar` / `char` of length 4001–8000 also maps to `NVARCHAR(MAX)` class (capacity preserved, never silently narrowed), `varbinary` / `image` / `rowversion` → `VARBINARY(MAX)`, `decimal` / `numeric` / `money` → `DECIMAL`; `datetime2` is read via `CONVERT(nvarchar(33), col, 126)` and `time` via `CONVERT(nvarchar(16), col, 126)` for exactness.
41
+ - **ro read-only is weaker than MySQL/PG**: SQL Server has no equivalent session-level read-only statement. Under an ro grant the adapter's query gate has five checks: ① a first-word allowlist (`select / with / show / describe / desc / explain / use / set / help / table`); ② rejection of multiple statements (lexical: the moment anything follows a top-level semicolon); ③ rejection of `SELECT ... INTO` (token-level, so `into` inside strings, bracket identifiers, or comments is not a false positive); ④ rejection of `WITH ... (INSERT|UPDATE|DELETE|MERGE)`; ⑤ rejection of `OPENQUERY` / `OPENROWSET` / `OPENDATASOURCE` (their string arguments execute arbitrary statements remotely/unattended and cannot be audited, so even legitimate read-only remote queries are refused). The service layer also blocks `execute`, and the guard layer still requires confirmation for the same two "read head, write body" forms. **This is still application-level interception, not the same as a database-side read-only session**; use a database-side read-only account when you need strong isolation.
42
+ - **Data transfer**: writes use parameterized multi-row `VALUES` INSERTs (TDS caps a statement at 2100 parameters and a `VALUES` table constructor at 1000 rows, so rows per statement is the smaller of the two; replace/ignore go through `MERGE`). Transfer paging is version-split as well: 2012+ uses `OFFSET/FETCH`, while 2008/2008R2 degrades — ordered cursors (keyset/range) use `SELECT TOP (n) ... ORDER BY key`, and the offset full ordering plus `keyAt` chunking use `ROW_NUMBER() OVER (...)` subqueries; a failed version probe falls back to 0 and takes the conservative branch, so **transfer works on SQL Server 2008/2008R2 too**. Known limits: when the target table already exists and a column is IDENTITY, writing explicit values fails with error 544 (the plugin does not issue `SET IDENTITY_INSERT ON`); tables created by transfer do not generate IDENTITY columns. CREATE TABLE DDL can be rolled back inside an explicit transaction, so no Oracle/DM-style implicit-commit handling is needed.
43
+ - **Verification status**: this round ships offline unit tests only (Fake driver); the SQL Server path is **not verified against a live database**. The live smoke switch is `DBT_TEST_SQLSERVER_URL` (e.g. `sqlserver://sa:pw@host:1433/db`).
44
+ - **Driver packaging**: `mssql@12` lives in devDependencies only; at runtime the plugin loads `vendor/sqlserver-driver.cjs` built by `npm run build:sqlserver`. Reason: mssql → tedious → bl → readable-stream drags buffer/events/process/string_decoder into the production dependency tree, tripping the host's `dsh-app-boot` `require.resolve.paths` bug and failing the whole plugin load.
45
+ - **Error-message redaction**: when a connection URL fails to parse, the error text has the userinfo redacted and never echoes the plaintext password (errors reach `audit.jsonl` and the model context, so redaction is mandatory).
46
+ - **Not supported**: SQL Server 2005 (9.x) and earlier — neither supported nor verified; `tdsVersion=8_0` (TDS 8.0) and `encrypt='strict'` are also unsupported — SQL Server 2022 connects fine with the default `7_4`, and TDS 8.0 strict encryption is a later enhancement.
47
+
29
48
  ## Data Layout
30
49
 
31
50
  `$DSH_HOME/db-tool/` (0700):
@@ -39,7 +58,7 @@ The sidebar SQL console panel has been upgraded to a Navicat-style multi-tab edi
39
58
 
40
59
  ## Installation
41
60
 
42
- Drivers for all 8 databases ship with the npm package (SQLite prefers the built-in `node:sqlite`, optionally `better-sqlite3`; the GaussDB driver is the official Huawei Cloud npm package `gaussdb-node`) — no build steps of any kind. GaussDB authentication supports sha256 and md5; md5-sha256 hybrid and SM3 are not supported yet.
61
+ Drivers for all 9 databases ship with the npm package (SQLite prefers the built-in `node:sqlite`, optionally `better-sqlite3`; the GaussDB driver is the official Huawei Cloud npm package `gaussdb-node`) — no build steps of any kind. The SQL Server driver is the exception in form: `mssql@12` lives in devDependencies only, and the runtime loads the bundled `vendor/sqlserver-driver.cjs` built by `npm run build:sqlserver` — mssql's production dependency tree pulls in buffer/events/process/string_decoder, which trips the host's resolve.paths bug. GaussDB authentication supports sha256 and md5; md5-sha256 hybrid and SM3 are not supported yet.
43
62
 
44
63
  The only one-time interaction comes from oracledb: pnpm blocks its install script by default, which makes the first install report a failure — click **"Allow these scripts and retry"** in the DSH plugin installation UI to finish (the script only checks the Node version and prints a banner, so approving is risk-free; the approval is persisted to the profile and upgrades won't prompt again).
45
64
 
@@ -78,19 +97,19 @@ npx tsc --noEmit
78
97
  npx stryker run # mutation testing (lib/guard + lib/manager + lib/store, reports in reports/mutation/)
79
98
  ```
80
99
 
81
- Live-database smoke tests (run only when set): `DBT_TEST_MYSQL_URL / DBT_TEST_PG_URL / DBT_TEST_REDIS_URL / DBT_TEST_DM_CONNECT / DBT_TEST_MONGO_URL / DBT_TEST_ORACLE_CONNECT`. GaussDB, Oracle, and Mongo official requirements are implemented per their official docs; anything not verified against live databases is annotated in code.
100
+ Live-database smoke tests (run only when set): `DBT_TEST_MYSQL_URL / DBT_TEST_PG_URL / DBT_TEST_REDIS_URL / DBT_TEST_DM_CONNECT / DBT_TEST_MONGO_URL / DBT_TEST_ORACLE_CONNECT / DBT_TEST_SQLSERVER_URL` (SQL Server in the form `sqlserver://sa:pw@host:1433/db`). The GaussDB, Oracle/Mongo, and SQL Server requirements are implemented per their official docs; anything not verified against live databases is annotated in code. **This round ships offline unit tests only (Fake driver) for SQL Server — no live-database verification.**
82
101
 
83
102
  ## Project Structure
84
103
 
85
104
  ```
86
- lib/ host plugin (store / adapters×8 / guard / manager / http / index)
105
+ lib/ host plugin (store / adapters×9 / guard / manager / http / index)
87
106
  client/ sidebar single-file artifact (client.js — this is the source)
88
107
  skills/ db-admin skill
89
- scripts/ build & tooling scripts (MongoDB driver / CodeMirror editor bundles, DSH host hotfix patch:dsh, etc.)
108
+ scripts/ build & tooling scripts (MongoDB / SQL Server driver and CodeMirror editor bundles, DSH host hotfix patch:dsh, etc.)
90
109
  patches/ DSH host dsh-app-boot hotfix patches (selected by host version for patch:dsh)
91
110
  docs/ reserved (currently empty)
92
111
  tests/ vitest (offline mocks + DBT_TEST_*-gated live runs)
93
- vendor/ mongodb-driver.cjs and codemirror-sql.cjs bundles (gitignored; shipped via the files whitelist)
112
+ vendor/ mongodb-driver.cjs, sqlserver-driver.cjs and codemirror-sql.cjs bundles (gitignored; shipped via the files whitelist)
94
113
  ```
95
114
 
96
115
  ## License
package/client/client.js CHANGED
@@ -105,6 +105,7 @@ window.__ModuleLoader__.load({
105
105
  urlUser: "用户名(可选,自动注入 URL)",
106
106
  urlPassword: "密码(可选,自动注入 URL;留空保持已存)",
107
107
  fieldsHost: "主机地址",
108
+ fieldsHostInstance: "主机地址(命名实例填 主机\\实例名,无需端口)",
108
109
  fieldsPort: "端口",
109
110
  fieldsUser: "用户名",
110
111
  fieldsPassword: "密码",
@@ -323,6 +324,7 @@ window.__ModuleLoader__.load({
323
324
  urlUser: "Username (optional, injected into URL)",
324
325
  urlPassword: "Password (optional, injected into URL; blank keeps saved)",
325
326
  fieldsHost: "Host",
327
+ fieldsHostInstance: "Host (named instance: host\\instance, no port needed)",
326
328
  fieldsPort: "Port",
327
329
  fieldsUser: "User",
328
330
  fieldsPassword: "Password",
@@ -468,7 +470,7 @@ window.__ModuleLoader__.load({
468
470
 
469
471
  const TAB_ID = "dsh-db-tool";
470
472
  const API = "/dsh-db-tool/api";
471
- const DB_KINDS = ["mysql", "postgresql", "gaussdb", "sqlite", "redis", "mongodb", "oracle", "dmdb"];
473
+ const DB_KINDS = ["mysql", "postgresql", "gaussdb", "sqlite", "redis", "mongodb", "oracle", "dmdb", "sqlserver"];
472
474
  const PAGE_SIZE = 50; // 契约:preview limit≤50 服务端强制
473
475
 
474
476
  // --- HTTP 封装:保留 code / challengeId 供确认通道使用 ---
@@ -876,9 +878,10 @@ window.__ModuleLoader__.load({
876
878
  // 统一返回形状:{ ok:true, ops:[{statement, params?, database?}] }(多条顺序执行,如 redis 两步)
877
879
  // 或 { ok:false, error:"<i18n key>" }(error 为 key 名,组件侧 t() 本地化;null 表示通用错误)
878
880
 
879
- // 方言判定:q=问号占位符(mysql/sqlite)、dollar=$n(postgresql/gaussdb)、colon=:n(oracle/dmdb)
881
+ // 方言判定:q=问号占位符(mysql/sqlite/sqlserver,sqlserver 的 @pN 由适配器内部重写)、
882
+ // dollar=$n(postgresql/gaussdb)、colon=:n(oracle/dmdb)
880
883
  function dialectOf(kind) {
881
- if (kind === "mysql" || kind === "sqlite") return "q";
884
+ if (kind === "mysql" || kind === "sqlite" || kind === "sqlserver") return "q";
882
885
  if (kind === "postgresql" || kind === "gaussdb") return "dollar";
883
886
  if (kind === "oracle" || kind === "dmdb") return "colon";
884
887
  if (kind === "redis") return "redis";
@@ -886,8 +889,10 @@ window.__ModuleLoader__.load({
886
889
  return null;
887
890
  }
888
891
 
889
- // 标识符引用:mysql 用反引号(内部双写转义),其余统一双引号(内部双写转义)
892
+ // 标识符引用:mysql 用反引号(内部双写转义),sqlserver 用方括号(] 双写转义),
893
+ // 其余统一双引号(内部双写转义)
890
894
  function quoteIdent(kind, name) {
895
+ if (kind === "sqlserver") return "[" + String(name).split("]").join("]]") + "]";
891
896
  const q = kind === "mysql" ? "`" : "\"";
892
897
  return q + String(name).split(q).join(q + q) + q;
893
898
  }
@@ -920,7 +925,9 @@ window.__ModuleLoader__.load({
920
925
  return { ok: false, error: "noPkHint" };
921
926
  }
922
927
  // 表全限定:pg/gauss 用 schema 前缀(库由 execute 的 database 参数路由,语句内不写库名);
923
- // mysql 带库名(adapter 无 database 路由);oracle/dmdb 的 dbRef 即 OWNER;sqlite 仅表名
928
+ // mysql 表名同样带库名(execute 的 database 参数已按库路由,全限定在目标库上执行等价,双保险);
929
+ // oracle/dmdb 的 dbRef 即 OWNER;sqlite 仅表名;
930
+ // sqlserver 有 schema 时写 [库].[schema].[表] 三段
924
931
  // execute 的 database 参数只传库名(db.schema 形态取点前段),连接目标只能是库,schema 靠语句内全限定
925
932
  let table;
926
933
  if (kind === "postgresql" || kind === "gaussdb") {
@@ -929,6 +936,13 @@ window.__ModuleLoader__.load({
929
936
  : quoteIdent(kind, tableName);
930
937
  } else if (kind === "mysql" || kind === "oracle" || kind === "dmdb") {
931
938
  table = quoteIdent(kind, dbRef) + "." + quoteIdent(kind, tableName);
939
+ } else if (kind === "sqlserver") {
940
+ // 无 schema 时只写表名:T-SQL 两段式是 schema.object(不是 database.object),
941
+ // 写 [库].[表] 会被解析成 schema=库名 → 报 208 对象不存在;库由 database 参数路由
942
+ // (适配器按库名切池),与 pg/gauss 分支同构。
943
+ table = schemaName
944
+ ? quoteIdent(kind, String(dbRef).split(".")[0]) + "." + quoteIdent(kind, schemaName) + "." + quoteIdent(kind, tableName)
945
+ : quoteIdent(kind, tableName);
932
946
  } else {
933
947
  table = quoteIdent(kind, tableName);
934
948
  }
@@ -1029,23 +1043,29 @@ window.__ModuleLoader__.load({
1029
1043
  if (kind === "postgresql" || kind === "gaussdb") return "postgresql";
1030
1044
  if (kind === "sqlite") return "sqlite";
1031
1045
  if (kind === "oracle" || kind === "dmdb") return "plsql";
1046
+ if (kind === "sqlserver") return "transactsql";
1032
1047
  return null;
1033
1048
  }
1034
1049
 
1035
1050
  // SQL 文本 → 语句列表 [{ text, start }]:text 为原文切片(保留内部注释/空白),start 为语句
1036
1051
  // 首字符偏移(跳过前导空白/注释,错误就地显示后定位光标用)。词法跳过与 lang-sql 方言 spec
1037
- // 一一对应:单/双引号字符串(反斜杠与双写转义,mysql 另含反引号标识符)、-- 行注释
1038
- // (slashComments)、/* */ 块注释、$$...$$ dollar-quoting(doubleDollarQuotedStrings,仅
1052
+ // 一一对应:单/双引号字符串(双写转义;反斜杠转义是 MySQL 专有语义,其它 kind 按标准 SQL)、
1053
+ // -- 行注释(slashComments)、/* */ 块注释、$$...$$ dollar-quoting(doubleDollarQuotedStrings,仅
1039
1054
  // postgresql/gaussdb)、MySQL DELIMITER 自定义分隔符(客户端指令:仅语句缓冲为空时切换后续
1040
1055
  // 分隔符,指令行不产生语句)、Oracle/达梦 BEGIN..END 块(块内分号不切分;CREATE
1041
- // PROCEDURE/FUNCTION 体的分号由该块覆盖)。空/纯注释语句被过滤;无尾分隔符的最后一条也返回。
1056
+ // PROCEDURE/FUNCTION 体的分号由该块覆盖)、SQL Server [方括号] 标识符与 GO 批分隔符
1057
+ // (GO 支持重复计数与行尾 -- 注释;GO n 按总数语义展开上一批)。
1058
+ // 空/纯注释语句被过滤;无尾分隔符的最后一条也返回。
1042
1059
  // ponytail: depth 计数不识别 CASE..END/LOOP/IF 等(块内混用可能提前切分);需要时换完整 PL/SQL 词法
1060
+ // GO n 的重复上限:GO 是客户端批分隔符,n 次展开会成倍放大执行请求数,钳制防手滑/恶意写出天量请求
1061
+ const GO_REPEAT_MAX = 1000;
1043
1062
  function splitSqlStatements(sql, kind) {
1044
1063
  const src = String(sql == null ? "" : sql);
1045
1064
  const out = [];
1046
1065
  const isWord = (c) => /[A-Za-z0-9_$]/.test(c);
1047
1066
  let sep = ";";
1048
1067
  let i = 0, stmtStart = 0, contentStart = 0, hasContent = false, depth = 0; // depth: BEGIN..END 嵌套计数
1068
+ let batchFrom = 0; // 当前批(GO 之间)在 out 中的起始下标,供 GO n 整批重复用
1049
1069
  while (i < src.length) {
1050
1070
  const c = src[i];
1051
1071
  if (c === "-" && src[i + 1] === "-") { // -- 行注释至行尾
@@ -1058,12 +1078,14 @@ window.__ModuleLoader__.load({
1058
1078
  i = Math.min(src.length, i + 2);
1059
1079
  continue;
1060
1080
  }
1061
- if (c === "'" || c === '"' || (kind === "mysql" && c === "`")) { // 引号串:反斜杠与双写转义
1081
+ if (c === "'" || c === '"' || (kind === "mysql" && c === "`")) { // 引号串:双写转义;反斜杠转义仅 MySQL 语义
1062
1082
  if (!hasContent) contentStart = i;
1063
1083
  hasContent = true;
1064
1084
  i++;
1065
1085
  while (i < src.length) {
1066
- if (src[i] === "\\") { i += 2; continue; }
1086
+ // 反斜杠转义是 MySQL 专有语义;其它 kind(pg/oracle/sqlite/sqlserver)按标准 SQL,
1087
+ // \ 是普通字符(T-SQL 下 'C:\temp\' 正常闭合,否则会误吞后续语句)
1088
+ if (kind === "mysql" && src[i] === "\\") { i += 2; continue; }
1067
1089
  if (src[i] === c) {
1068
1090
  if (src[i + 1] === c) { i += 2; continue; } // 双写转义
1069
1091
  i++;
@@ -1073,6 +1095,21 @@ window.__ModuleLoader__.load({
1073
1095
  }
1074
1096
  continue;
1075
1097
  }
1098
+ // [方括号] 标识符(sqlserver):内部 ; 不切分,]] 为转义(方括号内反斜杠无转义语义)
1099
+ if (kind === "sqlserver" && c === "[") {
1100
+ if (!hasContent) contentStart = i;
1101
+ hasContent = true;
1102
+ i++;
1103
+ while (i < src.length) {
1104
+ if (src[i] === "]") {
1105
+ if (src[i + 1] === "]") { i += 2; continue; }
1106
+ i++;
1107
+ break;
1108
+ }
1109
+ i++;
1110
+ }
1111
+ continue;
1112
+ }
1076
1113
  if ((kind === "postgresql" || kind === "gaussdb") && c === "$" && src[i + 1] === "$") {
1077
1114
  if (!hasContent) contentStart = i; // $$dollar 串至下一 $$(未闭合则吞到结尾)
1078
1115
  hasContent = true;
@@ -1091,6 +1128,50 @@ window.__ModuleLoader__.load({
1091
1128
  while (j < e && (src[j] === " " || src[j] === "\t")) j++;
1092
1129
  if (j < e) { sep = src.slice(j, e); stmtStart = e; i = e; continue; }
1093
1130
  }
1131
+ // GO 批分隔符(sqlserver):独占一行——行首可缩进、行尾可跟空白与 -- 行注释,
1132
+ // 可带重复计数(GO 3 = 上一批执行 3 次,总数语义,与 sqlcmd 一致);大小写不敏感;
1133
+ // GO 行本身不产生语句。计数超 GO_REPEAT_MAX 时截断(防天量请求)。
1134
+ if (kind === "sqlserver" && depth === 0 && (i === 0 || src[i - 1] === "\n")) {
1135
+ let j = i;
1136
+ while (j < src.length && (src[j] === " " || src[j] === "\t")) j++;
1137
+ if (src.slice(j, j + 2).toUpperCase() === "GO" && !isWord(src[j + 2] || " ")) {
1138
+ let k = j + 2, count = 1;
1139
+ while (k < src.length && (src[k] === " " || src[k] === "\t")) k++;
1140
+ if (src[k] >= "0" && src[k] <= "9") { // 可选重复计数:GO 0 / GO 1 均等同 1 次
1141
+ let n = 0;
1142
+ while (k < src.length && src[k] >= "0" && src[k] <= "9") {
1143
+ n = Math.min(n * 10 + (src.charCodeAt(k) - 48), GO_REPEAT_MAX);
1144
+ k++;
1145
+ }
1146
+ count = Math.max(1, n);
1147
+ }
1148
+ while (k < src.length && (src[k] === " " || src[k] === "\t")) k++;
1149
+ if (src[k] === "-" && src[k + 1] === "-") { // 行尾注释:GO -- 说明
1150
+ while (k < src.length && src[k] !== "\n") k++;
1151
+ }
1152
+ if (k >= src.length || src[k] === "\n" || src[k] === "\r") { // 确认行尾:行内 GO 不算
1153
+ if (hasContent) {
1154
+ // 语句切到 GO 行首前一行的行尾换行之前(text 不带 GO 行前导换行)
1155
+ let end = i;
1156
+ if (end > stmtStart && src[end - 1] === "\n") end -= src[end - 2] === "\r" ? 2 : 1;
1157
+ out.push({ text: src.slice(stmtStart, end), start: contentStart });
1158
+ }
1159
+ // 批重复:把刚切出的当前批再追加 count-1 遍(总数语义;复制对象,不共享引用)
1160
+ if (count > 1 && out.length > batchFrom) {
1161
+ const batch = out.slice(batchFrom);
1162
+ for (let r = 1; r < count; r++) {
1163
+ for (const s of batch) out.push({ text: s.text, start: s.start });
1164
+ }
1165
+ }
1166
+ batchFrom = out.length; // 下一批从后续新语句开始
1167
+ i = k;
1168
+ stmtStart = k;
1169
+ contentStart = k;
1170
+ hasContent = false;
1171
+ continue;
1172
+ }
1173
+ }
1174
+ }
1094
1175
  // BEGIN/END 词计数(oracle/dmdb):块内分号不切分;END 不把 depth 减到负(顶层 CASE..END 不受影响)
1095
1176
  if ((kind === "oracle" || kind === "dmdb") && (i === 0 || !isWord(src[i - 1]))) {
1096
1177
  if (src.slice(i, i + 5).toUpperCase() === "BEGIN" && !isWord(src[i + 5] || " ")) {
@@ -1172,6 +1253,7 @@ window.__ModuleLoader__.load({
1172
1253
  mongodb: { host: "127.0.0.1", port: 27017, database: "test" },
1173
1254
  oracle: { host: "127.0.0.1", port: 1521, user: "system" },
1174
1255
  dmdb: { host: "127.0.0.1", port: 5236, user: "SYSDBA" },
1256
+ sqlserver: { host: "127.0.0.1", user: "sa" },
1175
1257
  };
1176
1258
 
1177
1259
  /**
@@ -1188,6 +1270,24 @@ window.__ModuleLoader__.load({
1188
1270
  return next;
1189
1271
  }
1190
1272
 
1273
+ /** URL→分字段的字段反解:只取可信部分。safeUrl 仅脱敏密码段(用户名/主机/端口为真值),
1274
+ * 密码不可反解——留空走编辑语义,clearUrl 时由服务端迁移 URL 内嵌密码。非法 URL 返回 null。 */
1275
+ function urlToFieldsPatch(url) {
1276
+ if (!url) return null;
1277
+ try {
1278
+ const u = new URL(url);
1279
+ const decoded = (s) => { try { return decodeURIComponent(s); } catch { return s; } };
1280
+ return {
1281
+ host: u.hostname,
1282
+ port: u.port,
1283
+ user: decoded(u.username),
1284
+ database: decoded(u.pathname.replace(/^\//, "")),
1285
+ };
1286
+ } catch {
1287
+ return null;
1288
+ }
1289
+ }
1290
+
1191
1291
  /** 新建表单初始态:分字段模式 + 当前 kind 的官方默认值 */
1192
1292
  const freshForm = () => withKindDefaults({ ...EMPTY_FORM, mode: "fields" });
1193
1293
 
@@ -1242,12 +1342,20 @@ window.__ModuleLoader__.load({
1242
1342
  setDirty(new Set());
1243
1343
  setDraftTest(null);
1244
1344
  }
1245
- /** 从 URL 模式切回分字段:保留表单里已有值(编辑回填场景),只对空字段补官方默认值 */
1345
+ /** 从 URL 模式切回分字段:表单已有值保留;空字段先从已填 URL 反解真实值,仍空的才补官方默认值——
1346
+ * 直接补默认会把 URL 里的真实主机/端口悄悄换成 127.0.0.1/1433(丢端口 bug)。 */
1246
1347
  function reenterFields() {
1247
1348
  setForm((prev) => {
1248
1349
  const keep = new Set(dirty);
1249
1350
  for (const k of ["host", "port", "user", "database"]) if (prev[k]) keep.add(k);
1250
- return withKindDefaults(Object.assign({}, prev, { mode: "fields" }), keep);
1351
+ const next = Object.assign({}, prev, { mode: "fields" });
1352
+ const fromUrl = urlToFieldsPatch(prev.url);
1353
+ if (fromUrl) {
1354
+ for (const k of ["host", "port", "user", "database"]) {
1355
+ if (!prev[k] && fromUrl[k]) { next[k] = fromUrl[k]; keep.add(k); }
1356
+ }
1357
+ }
1358
+ return withKindDefaults(next, keep);
1251
1359
  });
1252
1360
  setDraftTest(null);
1253
1361
  }
@@ -1268,7 +1376,8 @@ window.__ModuleLoader__.load({
1268
1376
  if (up) body.urlPassword = up;
1269
1377
  } else {
1270
1378
  body.fields = { host: form.host, user: form.user, database: form.database || undefined };
1271
- if (form.port) body.fields.port = Number(form.port);
1379
+ // 端口原样发字符串:Number("abc")=NaN 会被 JSON 序列化成 null,服务端按「未提供端口」静默回退默认值,配置错误被掩盖
1380
+ if (form.port) body.fields.port = form.port;
1272
1381
  if (form.password) body.fields.password = form.password;
1273
1382
  // 从 url 方式切到分字段保存 → 服务端清除已存 url(否则连接仍走旧 url)
1274
1383
  if (props.initial && props.initial.mode === "url") body.clearUrl = true;
@@ -1326,7 +1435,8 @@ window.__ModuleLoader__.load({
1326
1435
  "div",
1327
1436
  { style: { display: "flex", flexDirection: "column", gap: 6 } },
1328
1437
  React.createElement("div", { className: "dbt-row" },
1329
- React.createElement("input", { placeholder: t("fieldsHost"), "aria-label": t("fieldsHost") + t("requiredMark"), "aria-required": "true", ref: firstInputRef, value: form.host, onChange: (e) => patch({ host: e.target.value }) }),
1438
+ // sqlserver:官方模型 port 可选(默认 1433)且与命名实例互斥——host 直接填 主机\实例名,端口留空
1439
+ React.createElement("input", { placeholder: form.kind === "sqlserver" ? t("fieldsHostInstance") : t("fieldsHost"), "aria-label": t("fieldsHost") + t("requiredMark"), "aria-required": "true", ref: firstInputRef, value: form.host, onChange: (e) => patch({ host: e.target.value }) }),
1330
1440
  React.createElement("input", { placeholder: t("fieldsPort"), "aria-label": t("fieldsPort"), value: form.port, onChange: (e) => patch({ port: e.target.value }) }),
1331
1441
  React.createElement("input", { placeholder: t("fieldsDatabase"), "aria-label": t("fieldsDatabase"), value: form.database, onChange: (e) => patch({ database: e.target.value }) })),
1332
1442
  React.createElement("div", { className: "dbt-row" },
@@ -1732,8 +1842,8 @@ window.__ModuleLoader__.load({
1732
1842
  if (!open["c:" + c.id]) toggleConn(c);
1733
1843
  }
1734
1844
  }, [conns, projectPath, autoExpand]); // eslint-disable-line
1735
- // PG/GaussDB 官方层级为 数据库 → 模式(schema) → 表:库节点下先列 schema 再列表
1736
- const HAS_SCHEMAS = { postgresql: true, gaussdb: true };
1845
+ // PG/GaussDB/SQL Server 官方层级为 数据库 → 模式(schema) → 表:库节点下先列 schema 再列表
1846
+ const HAS_SCHEMAS = { postgresql: true, gaussdb: true, sqlserver: true };
1737
1847
  function toggleDb(c, d) {
1738
1848
  if (!projectPath) return;
1739
1849
  toggle("d:" + c.id + "/" + d, () => loadByKey("d:" + c.id + "/" + d));
@@ -2397,7 +2507,7 @@ window.__ModuleLoader__.load({
2397
2507
  };
2398
2508
  }
2399
2509
 
2400
- const CONSOLE_SQL_KINDS = { mysql: true, postgresql: true, gaussdb: true, sqlite: true, oracle: true, dmdb: true };
2510
+ const CONSOLE_SQL_KINDS = { mysql: true, postgresql: true, gaussdb: true, sqlite: true, oracle: true, dmdb: true, sqlserver: true };
2401
2511
 
2402
2512
  function ConsoleView(props) {
2403
2513
  const { conns, grants, projectPath, askConfirm, onError } = props;
@@ -2408,6 +2518,9 @@ window.__ModuleLoader__.load({
2408
2518
  // Navicat 式跨库操控:当前库下拉(""=连接默认库),查询/执行路由到所选库
2409
2519
  const [dbList, setDbList] = React.useState([]);
2410
2520
  const [db, setDb] = React.useState("");
2521
+ // 列库失败/加载中必须就地可见(对齐 BrowseTree 错误行;此前静默吞错,下拉在失败时无声消失)
2522
+ const [dbsErr, setDbsErr] = React.useState("");
2523
+ const [dbsLoading, setDbsLoading] = React.useState(false);
2411
2524
  // 多标签(内存态,不落盘):tabs[{id,title,sql,result,history}]
2412
2525
  const tabSeq = React.useRef(1);
2413
2526
  const [tabs, setTabs] = React.useState(() => [{ id: "t1", title: t("queryTab", { n: 1 }), sql: "", result: null, history: [] }]);
@@ -2433,11 +2546,19 @@ window.__ModuleLoader__.load({
2433
2546
  const activeTab = tabs[activeIdx] || tabs[0];
2434
2547
  const activeTabId = activeTab ? activeTab.id : "";
2435
2548
 
2436
- // 跨库下拉(保留既有行为:换连接重置)
2549
+ // 跨库下拉(保留既有行为:换连接重置);列库失败可见+可重试(词条与 TransferView 复用)
2550
+ function loadDbs() {
2551
+ setDbsErr(""); // 发起即清:换连接/重试成功/重试失败三路径由此闭合
2552
+ if (!connId || !projectPath) return;
2553
+ setDbsLoading(true);
2554
+ api("databases" + qs({ project: projectPath, connId })).then(
2555
+ (l) => { setDbList(l || []); setDbsLoading(false); },
2556
+ (e) => { setDbsErr(String(e && e.message ? e.message : e)); setDbsLoading(false); },
2557
+ );
2558
+ }
2437
2559
  React.useEffect(() => {
2438
2560
  setDbList([]); setDb("");
2439
- if (!connId || !projectPath) return;
2440
- api("databases" + qs({ project: projectPath, connId })).then((l) => setDbList(l || []), () => setDbList([]));
2561
+ loadDbs();
2441
2562
  }, [connId, projectPath]);
2442
2563
 
2443
2564
  // vendor bundle 异步加载(失败降级 textarea,静默)
@@ -2716,9 +2837,13 @@ window.__ModuleLoader__.load({
2716
2837
  const suffix = g && g.mode ? " · " + g.mode : "";
2717
2838
  return React.createElement("option", { key: c.id, value: c.id }, (c.name || c.id) + " (" + c.kind + suffix + ")");
2718
2839
  })),
2840
+ dbsErr && connId ? React.createElement("span", { className: "dbt-err", role: "alert" },
2841
+ t("trxLoadDbsFail") + ":" + dbsErr + " ",
2842
+ React.createElement("button", { className: "dbt-btn", onClick: loadDbs }, t("retry"))) : null,
2719
2843
  dbList.length > 1 ? React.createElement("select", { value: db, disabled: !!txToken, onChange: (e) => setDb(e.target.value) },
2720
2844
  React.createElement("option", { value: "" }, t("defaultDb")),
2721
2845
  dbList.map((d) => React.createElement("option", { key: d, value: d }, d))) : null,
2846
+ dbsLoading ? React.createElement("span", { className: "dbt-muted" }, "…") : null,
2722
2847
  ),
2723
2848
  ),
2724
2849
  // 多标签条(内存态):标签 + 关闭 + 新建
@@ -2896,8 +3021,8 @@ window.__ModuleLoader__.load({
2896
3021
  const srcConn = conns.find((c) => c.id === srcId);
2897
3022
  const dstConn = conns.find((c) => c.id === dstId);
2898
3023
  const dstGrant = (props.grants || []).find((x) => x.connId === dstId);
2899
- // PG/GaussDB 三层(库→模式→表),与 BrowsePane 同一张表;sqlite 无库层(连接默认上下文)
2900
- const HAS_SCHEMAS = { postgresql: true, gaussdb: true };
3024
+ // PG/GaussDB/SQL Server 三层(库→模式→表),与 BrowsePane 同一张表;sqlite 无库层(连接默认上下文)
3025
+ const HAS_SCHEMAS = { postgresql: true, gaussdb: true, sqlserver: true };
2901
3026
  const NO_DB_LEVEL = { sqlite: true };
2902
3027
 
2903
3028
  // 项目切换清层级缓存(对齐 BrowsePane:避免陈旧授权下的旧数据)
@@ -3590,6 +3715,7 @@ window.__ModuleLoader__.load({
3590
3715
  sortRows, slicePage, ResultSetGrid,
3591
3716
  classifyHead, ConsoleView,
3592
3717
  Panel, TransferView, TransferLogView,
3718
+ urlToFieldsPatch,
3593
3719
  };
3594
3720
  return module.exports;
3595
3721
  },
@@ -20,6 +20,7 @@
20
20
  */
21
21
  import db from 'dmdb';
22
22
  import { sqlHead, SQL_READ_HEADS } from '../../guard/index.js';
23
+ import { validPort } from '../sql-shared/common.js';
23
24
  const ROWS_MAX = 500;
24
25
  const CELL_TRUNC = 1000;
25
26
  const DDL_KEYWORDS = new Set([
@@ -69,6 +70,10 @@ export function resolveDmConn(conn) {
69
70
  throw new Error('DM URL 应以 dm:// 开头,如 dm://user:pass@host:5236');
70
71
  }
71
72
  const u = new URL(conn.url);
73
+ // 端口段校验:'dm://host:0' 等非法值抛中文错误,不再透传驱动报晦涩的 EADDRNOTAVAIL
74
+ // (connectString 仍用原 URL 原样透传,不重组)
75
+ if (u.port)
76
+ validPort(u.port, 5236, 'dmdb');
72
77
  return {
73
78
  connectString: conn.url,
74
79
  user: decodeURIComponent(u.username),
@@ -77,7 +82,9 @@ export function resolveDmConn(conn) {
77
82
  }
78
83
  const f = conn.fields ?? {};
79
84
  const host = typeof f.host === 'string' ? f.host : '';
80
- const port = typeof f.port === 'number' ? f.port : 5236;
85
+ // 前端原样发字符串端口:显式校验;历史 typeof==='number' 门把一切字符串(含合法 '14330')
86
+ // 静默丢弃回退 5236,且驱动侧 parseInt 对非法形态还有第二重静默回退
87
+ const port = validPort(f.port, 5236, 'dmdb');
81
88
  if (host === '') {
82
89
  throw new Error('DM 连接需要 url(dm://user:pass@host:5236)或 fields(host/port)');
83
90
  }
@@ -7,6 +7,7 @@ const LOADERS = {
7
7
  mongodb: () => import('./mongodb/index.js').then((m) => m.createMongoAdapter),
8
8
  oracle: () => import('./oracle/index.js').then((m) => m.createOracleAdapter),
9
9
  dmdb: () => import('./dmdb/index.js').then((m) => m.createDmAdapter),
10
+ sqlserver: () => import('./sqlserver/index.js').then((m) => m.createSqlserverAdapter),
10
11
  };
11
12
  /** 驱动缺失时的安装指引 */
12
13
  const INSTALL_HINTS = {
@@ -18,6 +19,7 @@ const INSTALL_HINTS = {
18
19
  mongodb: 'npm run build:mongodb(构建 vendor/mongodb-driver.cjs;发布包已内置产物,仅在缺失时需要)',
19
20
  oracle: 'npm install oracledb',
20
21
  dmdb: 'npm install dmdb',
22
+ sqlserver: 'npm run build:sqlserver(重建 vendor/sqlserver-driver.cjs;mssql 仅在 devDependencies)',
21
23
  };
22
24
  /** 生成带安装指引的驱动缺失错误 */
23
25
  export function driverMissingError(kind, cause) {
@@ -17,6 +17,7 @@
17
17
  * bson 保持 dependencies(bundle 的 external),保证 BSON 类实例与外部一致。
18
18
  */
19
19
  import { createRequire } from 'node:module';
20
+ import { validPort } from '../sql-shared/common.js';
20
21
  let vendorCache;
21
22
  /** 容错加载 vendor bundle:缺失时返回 undefined(调用方降级处理),成功后缓存。
22
23
  * 失败不缓存 → vendor 构建完成后无需重启即可恢复。 */
@@ -574,7 +575,9 @@ export function reviveEjson(v) {
574
575
  }
575
576
  export function buildUri(fields, ssl) {
576
577
  const host = typeof fields?.host === 'string' && fields.host !== '' ? fields.host : '127.0.0.1';
577
- const port = typeof fields?.port === 'number' ? fields.port : 27017;
578
+ // 前端原样发字符串端口:显式校验;历史 typeof==='number' 门把一切字符串(含合法 '14330')
579
+ // 静默丢弃回退 27017,分字段模式用户所填端口实际永不生效
580
+ const port = validPort(fields?.port, 27017, 'mongodb');
578
581
  const user = typeof fields?.user === 'string' ? encodeURIComponent(fields.user) : '';
579
582
  const pass = typeof fields?.password === 'string' ? `:${encodeURIComponent(fields.password)}` : '';
580
583
  const auth = user !== '' ? `${user}${pass}@` : '';