dsh-db-tool 0.1.5 → 0.1.7

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/client/client.js CHANGED
@@ -38,6 +38,7 @@ window.__ModuleLoader__.load({
38
38
  fieldsPort: "端口",
39
39
  fieldsUser: "用户名",
40
40
  fieldsPassword: "密码",
41
+ passwordSaved: "已保存(留空保持不变)",
41
42
  fieldsDatabase: "数据库名",
42
43
  ssl: "启用 SSL",
43
44
  test: "测试",
@@ -68,6 +69,7 @@ window.__ModuleLoader__.load({
68
69
  noDatabases: "无可用数据库",
69
70
  noSchemas: "无模式",
70
71
  noTables: "无表",
72
+ defaultDb: "默认库",
71
73
  loadFailed: "加载失败",
72
74
  structure: "结构",
73
75
  preview: "数据预览",
@@ -125,6 +127,7 @@ window.__ModuleLoader__.load({
125
127
  fieldsPort: "Port",
126
128
  fieldsUser: "User",
127
129
  fieldsPassword: "Password",
130
+ passwordSaved: "Saved (leave blank to keep)",
128
131
  fieldsDatabase: "Database",
129
132
  ssl: "Enable SSL",
130
133
  test: "Test",
@@ -153,6 +156,7 @@ window.__ModuleLoader__.load({
153
156
  noDatabases: "No databases available",
154
157
  noSchemas: "No schemas",
155
158
  noTables: "No tables",
159
+ defaultDb: "Default DB",
156
160
  loadFailed: "Failed to load",
157
161
  structure: "Structure",
158
162
  preview: "Preview",
@@ -445,6 +449,20 @@ window.__ModuleLoader__.load({
445
449
  /** 新建表单初始态:分字段模式 + 当前 kind 的官方默认值 */
446
450
  const freshForm = () => withKindDefaults({ ...EMPTY_FORM, mode: "fields" });
447
451
 
452
+ /** 编辑回填:从 meta 还原已存配置(密码不出库,留空=保留原密码)。
453
+ * url 方式 → 回填脱敏 url;分字段方式 → 回填 host/port/user/database。 */
454
+ function buildEditForm(c) {
455
+ const mode = c.mode || (c.safeUrl ? "url" : "fields");
456
+ return {
457
+ ...EMPTY_FORM,
458
+ id: c.id, kind: c.kind, name: c.name || "", mode,
459
+ url: c.safeUrl || "",
460
+ host: c.host || "", port: c.port != null ? String(c.port) : "",
461
+ user: c.user || "", database: c.database || "",
462
+ ssl: !!c.ssl,
463
+ };
464
+ }
465
+
448
466
  function ConnForm(props) {
449
467
  // 新建(无 initial)默认分字段模式并预填官方默认值;编辑保持用户数据原样
450
468
  const [form, setForm] = React.useState(() => props.initial || freshForm());
@@ -459,18 +477,31 @@ window.__ModuleLoader__.load({
459
477
  setDirty(new Set());
460
478
  setDraftTest(null);
461
479
  }
462
- /** 从 URL 模式切回分字段:按当前 kind 重新补全官方默认值(未手改字段) */
480
+ /** 从 URL 模式切回分字段:保留表单里已有值(编辑回填场景),只对空字段补官方默认值 */
463
481
  function reenterFields() {
464
- setForm((prev) => withKindDefaults(Object.assign({}, prev, { mode: "fields" }), dirty));
482
+ setForm((prev) => {
483
+ const keep = new Set(dirty);
484
+ for (const k of ["host", "port", "user", "database"]) if (prev[k]) keep.add(k);
485
+ return withKindDefaults(Object.assign({}, prev, { mode: "fields" }), keep);
486
+ });
465
487
  setDraftTest(null);
466
488
  }
467
489
  function draftBody() {
468
490
  const body = { kind: form.kind, ssl: !!form.ssl };
469
- if (form.mode === "url") body.url = form.url;
470
- else {
491
+ // 编辑已有连接:透传 connId,服务端测试草稿时拼回已存机密(密码留空/url 未改动语义)
492
+ if (props.initial) body.connId = form.id;
493
+ // 编辑保存语义:url 模式未改动(仍等于回填的脱敏 url)→ 不发 url,保留 secrets 原值
494
+ const origUrl = props.initial && props.initial.mode === "url" ? props.initial.url : undefined;
495
+ if (form.mode === "url") {
496
+ if (form.url && form.url !== origUrl) body.url = form.url;
497
+ // 新建 url 连接必须发
498
+ if (!props.initial && form.url) body.url = form.url;
499
+ } else {
471
500
  body.fields = { host: form.host, user: form.user, database: form.database || undefined };
472
501
  if (form.port) body.fields.port = Number(form.port);
473
502
  if (form.password) body.fields.password = form.password;
503
+ // 从 url 方式切到分字段保存 → 服务端清除已存 url(否则连接仍走旧 url)
504
+ if (props.initial && props.initial.mode === "url") body.clearUrl = true;
474
505
  }
475
506
  return body;
476
507
  }
@@ -520,7 +551,7 @@ window.__ModuleLoader__.load({
520
551
  React.createElement("input", { placeholder: t("fieldsDatabase"), value: form.database, onChange: (e) => patch({ database: e.target.value }) })),
521
552
  React.createElement("div", { className: "dbt-row" },
522
553
  React.createElement("input", { placeholder: t("fieldsUser"), value: form.user, onChange: (e) => patch({ user: e.target.value }) }),
523
- React.createElement("input", { type: "password", placeholder: t("fieldsPassword"), value: form.password, onChange: (e) => patch({ password: e.target.value }) })),
554
+ React.createElement("input", { type: "password", placeholder: props.initial && props.initial.hasPassword ? t("passwordSaved") : t("fieldsPassword"), value: form.password, onChange: (e) => patch({ password: e.target.value }) })),
524
555
  ),
525
556
  // 分组 3:操作
526
557
  React.createElement(
@@ -583,7 +614,7 @@ window.__ModuleLoader__.load({
583
614
 
584
615
  if (editing) {
585
616
  return React.createElement(ConnForm, {
586
- initial: editing === "new" ? null : Object.assign({}, EMPTY_FORM, editing),
617
+ initial: editing === "new" ? null : buildEditForm(editing),
587
618
  busy: busy === "save",
588
619
  onSubmit: saveConn,
589
620
  onCancel: () => setEditing(null),
@@ -909,6 +940,14 @@ window.__ModuleLoader__.load({
909
940
  const [params, setParams] = React.useState("");
910
941
  const [result, setResult] = React.useState(null); // {kind:'query',...}|{kind:'exec',...}
911
942
  const [busy, setBusy] = React.useState(false);
943
+ // Navicat 式跨库操控:当前库下拉(""=连接默认库),查询/执行路由到所选库
944
+ const [dbList, setDbList] = React.useState([]);
945
+ const [db, setDb] = React.useState("");
946
+ React.useEffect(() => {
947
+ setDbList([]); setDb("");
948
+ if (!connId || !projectPath) return;
949
+ api("databases" + qs({ project: projectPath, connId })).then((l) => setDbList(l || []), () => setDbList([]));
950
+ }, [connId, projectPath]);
912
951
 
913
952
  async function run() {
914
953
  setBusy(true);
@@ -924,7 +963,7 @@ window.__ModuleLoader__.load({
924
963
  let parsedParams;
925
964
  if (params.trim()) { try { parsedParams = JSON.parse(params); } catch (e) { throw new Error(t("paramsJson") + ": " + e.message); } }
926
965
  const data = await runGuarded(
927
- (challengeId) => api("execute", { method: "POST", body: { projectPath, connId, statement: sql, params: parsedParams, challengeId } }),
966
+ (challengeId) => api("execute", { method: "POST", body: { projectPath, connId, statement: sql, params: parsedParams, database: db || undefined, challengeId } }),
928
967
  askConfirm,
929
968
  );
930
969
  setResult({ kind: "exec", message: (data && data.message) || "", affectedRows: data && data.affectedRows });
@@ -932,7 +971,7 @@ window.__ModuleLoader__.load({
932
971
  let parsedParams;
933
972
  if (params.trim()) { try { parsedParams = JSON.parse(params); } catch (e) { throw new Error(t("paramsJson") + ": " + e.message); } }
934
973
  const data = await runGuarded(
935
- (challengeId) => api("query", { method: "POST", body: { projectPath, connId, sql, params: parsedParams, challengeId } }),
974
+ (challengeId) => api("query", { method: "POST", body: { projectPath, connId, sql, params: parsedParams, database: db || undefined, challengeId } }),
936
975
  askConfirm,
937
976
  );
938
977
  setResult({ kind: "query", data });
@@ -971,6 +1010,10 @@ window.__ModuleLoader__.load({
971
1010
  React.createElement("select", { value: connId, onChange: (e) => setConnId(e.target.value) },
972
1011
  React.createElement("option", { value: "" }, t("viewManage") + "…"),
973
1012
  conns.map((c) => React.createElement("option", { key: c.id, value: c.id }, (c.name || c.id) + " (" + c.kind + ")"))),
1013
+ // 当前库(Navicat 式跨库:选中非默认库后 SQL 在该库执行,pg/gaussdb 按库路由)
1014
+ dbList.length > 1 ? React.createElement("select", { value: db, onChange: (e) => setDb(e.target.value) },
1015
+ React.createElement("option", { value: "" }, t("defaultDb")),
1016
+ dbList.map((d) => React.createElement("option", { key: d, value: d }, d))) : null,
974
1017
  ),
975
1018
  React.createElement(
976
1019
  "div",
@@ -142,9 +142,17 @@ export async function createPgLikeAdapter(kind, Driver, conn, opts) {
142
142
  const v = res.rows[0]?.v;
143
143
  return { ok: true, serverInfo: v == null ? kind : String(v) };
144
144
  }),
145
- query: (sql, params) => humanize(`${kind} 查询失败`, async () => toQueryResult(await run(sql, params ?? []))),
146
- execute: (statement, params) => humanize(`${kind} 执行失败`, async () => {
147
- const res = await run(statement, params ?? []);
145
+ query: (sql, params, database) => humanize(`${kind} 查询失败`, async () => {
146
+ // 跨库操控(Navicat 式):指定目标库 → 路由到该库的池(主池事务不跨库,忽略事务路由)
147
+ const res = database
148
+ ? await poolFor(assertIdent(database, '数据库')).query(sql, params ?? [])
149
+ : await run(sql, params ?? []);
150
+ return toQueryResult(res);
151
+ }),
152
+ execute: (statement, params, database) => humanize(`${kind} 执行失败`, async () => {
153
+ const res = database
154
+ ? await poolFor(assertIdent(database, '数据库')).query(statement, params ?? [])
155
+ : await run(statement, params ?? []);
148
156
  const n = res.rowCount;
149
157
  return {
150
158
  affectedRows: typeof n === 'number' ? n : undefined,
@@ -160,6 +160,8 @@ async function route(req, res, service, path, q, opts) {
160
160
  ...(b['url'] !== undefined ? { url: str(b['url']) } : {}),
161
161
  ...(b['fields'] !== undefined ? { fields: b['fields'] } : {}),
162
162
  ...(ssl !== undefined ? { ssl } : {}),
163
+ // 编辑已有连接时透传:服务端拼回已存机密(留空密码/url 未改动的测试语义)
164
+ ...(b['connId'] !== undefined ? { connId: optStr(b['connId']) } : {}),
163
165
  }));
164
166
  }
165
167
  const connMatch = /^\/api\/connections\/([^/]+)(\/test)?$/.exec(path);
@@ -177,6 +179,7 @@ async function route(req, res, service, path, q, opts) {
177
179
  ...(b['name'] !== undefined ? { name: str(b['name']) } : {}),
178
180
  ...(b['url'] !== undefined ? { url: str(b['url']) } : {}),
179
181
  ...(b['fields'] !== undefined ? { fields: b['fields'] } : {}),
182
+ ...(b['clearUrl'] === true ? { clearUrl: true } : {}),
180
183
  ...(ssl !== undefined ? { ssl } : {}),
181
184
  }));
182
185
  }
@@ -222,11 +225,11 @@ async function route(req, res, service, path, q, opts) {
222
225
  }
223
226
  if (path === '/api/query' && req.method === 'POST') {
224
227
  const b = await readBody(req);
225
- return await sendMaybeConfirm(res, service.query(projectOfBody(b), str(b['connId']), str(b['sql']), b['params'], optStr(b['challengeId'])));
228
+ return await sendMaybeConfirm(res, service.query(projectOfBody(b), str(b['connId']), str(b['sql']), b['params'], optStr(b['challengeId']), optStr(b['database'])));
226
229
  }
227
230
  if (path === '/api/execute' && req.method === 'POST') {
228
231
  const b = await readBody(req);
229
- return await sendMaybeConfirm(res, service.execute(projectOfBody(b), str(b['connId']), str(b['statement']), b['params'], optStr(b['challengeId'])));
232
+ return await sendMaybeConfirm(res, service.execute(projectOfBody(b), str(b['connId']), str(b['statement']), b['params'], optStr(b['challengeId']), optStr(b['database'])));
230
233
  }
231
234
  if (path === '/api/script' && req.method === 'POST') {
232
235
  const b = await readBody(req);
package/dist/index.js CHANGED
@@ -25,7 +25,7 @@ const TOOL_DESCRIPTION = `数据库管理工具(dsh-db-tool):在已配置
25
25
  action 说明:
26
26
  - list_connections:列出连接(脱敏,含各连接的 kind 与授权信息需另经 HTTP 面板查看)。
27
27
  - query:只读 SQL/命令(conn_id, sql, params?)。
28
- - execute:写操作/DDL(conn_id, statement, params?)。
28
+ - execute:写操作/DDL(conn_id, statement, params?, database? 跨库目标)。
29
29
  - schema:结构浏览(conn_id;给 table 查列、给 database 查表、都不给列库)。
30
30
  - preview:预览行,limit≤50(conn_id, table, database?, limit?)。
31
31
  - run_script:子进程沙箱脚本(conn_id, code),60s 超时强杀,permission model 禁文件
@@ -84,7 +84,7 @@ export function apply(ctx) {
84
84
  sql: { type: 'string', description: 'query 的只读 SQL/命令' },
85
85
  statement: { type: 'string', description: 'execute 的语句' },
86
86
  params: { type: 'array', items: {}, description: '占位符参数' },
87
- database: { type: 'string', description: '库/schema(可选)' },
87
+ database: { type: 'string', description: '库/schema(可选)。query/execute:指定目标库即跨库执行(Navicat 式,pg/gaussdb 支持按库路由);schema/preview:浏览目标(pg/gaussdb 传 "库名.schema")' },
88
88
  table: { type: 'string', description: '表/集合/键(schema/preview 用)' },
89
89
  limit: { type: 'number', description: 'preview 行数上限(≤50)' },
90
90
  offset: { type: 'number', description: 'preview 行偏移(翻页用,默认 0)' },
package/dist/manager.js CHANGED
@@ -75,15 +75,28 @@ export class DbToolService {
75
75
  /**
76
76
  * 测试未保存的连接草稿(侧边栏「保存前测试」)。不落库、不写审计,
77
77
  * 一次性适配器用完即关。url/fields 校验交给适配器层(与保存后测试同口径)。
78
+ * 编辑已有连接时传 connId:密码留空/url 未改动时自动拼回已存机密
79
+ * (仅注入本次测试,绝不回传客户端)。
78
80
  */
79
81
  async testDraft(input) {
80
82
  const rc = {
81
- meta: { id: '(draft)', kind: input.kind, name: '(draft)' },
83
+ meta: { id: input.connId ?? '(draft)', kind: input.kind, name: '(draft)' },
82
84
  };
83
- if (input.url !== undefined)
84
- rc.url = input.url;
85
- if (input.fields !== undefined)
86
- rc.fields = { ...input.fields };
85
+ let url = input.url;
86
+ let fields = input.fields !== undefined ? { ...input.fields } : undefined;
87
+ if (input.connId) {
88
+ // 已存连接:编辑时客户端不发旧机密(留空语义),这里从 secrets 拼回
89
+ const sec = this.store.secrets.get(input.connId);
90
+ if (url === undefined && sec?.url !== undefined)
91
+ url = sec.url;
92
+ if (fields !== undefined && sec?.password !== undefined && fields.password === undefined) {
93
+ fields.password = sec.password;
94
+ }
95
+ }
96
+ if (url !== undefined)
97
+ rc.url = url;
98
+ if (fields !== undefined)
99
+ rc.fields = fields;
87
100
  if (input.ssl !== undefined)
88
101
  rc.ssl = input.ssl;
89
102
  try {
@@ -190,7 +203,7 @@ export class DbToolService {
190
203
  }
191
204
  }
192
205
  /* -- SQL 控制台(guard + challenge) -- */
193
- async query(projectPath, connId, sql, params, challengeId) {
206
+ async query(projectPath, connId, sql, params, challengeId, database) {
194
207
  const statement = assertNonEmpty(sql, 'sql');
195
208
  return this.runGuarded({
196
209
  projectPath,
@@ -198,11 +211,11 @@ export class DbToolService {
198
211
  op: 'query',
199
212
  statement,
200
213
  challengeId,
201
- run: (adapter) => adapter.query(statement, params),
214
+ run: (adapter) => adapter.query(statement, params, database),
202
215
  rowsAffected: (r) => r.rowCount,
203
216
  });
204
217
  }
205
- async execute(projectPath, connId, statement, params, challengeId) {
218
+ async execute(projectPath, connId, statement, params, challengeId, database) {
206
219
  const stmt = assertNonEmpty(statement, 'statement');
207
220
  return this.runGuarded({
208
221
  projectPath,
@@ -210,7 +223,7 @@ export class DbToolService {
210
223
  op: 'execute',
211
224
  statement: stmt,
212
225
  challengeId,
213
- run: (adapter) => adapter.execute(stmt, params),
226
+ run: (adapter) => adapter.execute(stmt, params, database),
214
227
  rowsAffected: (r) => r.affectedRows,
215
228
  });
216
229
  }
@@ -429,11 +442,11 @@ export async function handleToolAction(service, args, projectPath) {
429
442
  case 'list_connections':
430
443
  return ok(j(service.listConnections()));
431
444
  case 'query': {
432
- const r = await service.query(projectPath, requireConn(connId), assertArg(args.sql, 'sql'), args.params, challengeId);
445
+ const r = await service.query(projectPath, requireConn(connId), assertArg(args.sql, 'sql'), args.params, challengeId, args.database);
433
446
  return ok(needConfirmText(r) ?? j(r));
434
447
  }
435
448
  case 'execute': {
436
- const r = await service.execute(projectPath, requireConn(connId), assertArg(args.statement ?? args.sql, 'statement'), args.params, challengeId);
449
+ const r = await service.execute(projectPath, requireConn(connId), assertArg(args.statement ?? args.sql, 'statement'), args.params, challengeId, args.database);
437
450
  return ok(needConfirmText(r) ?? j(r));
438
451
  }
439
452
  case 'schema': {
@@ -35,19 +35,29 @@ function splitPassword(fields) {
35
35
  const { [PASSWORD_KEY]: password, ...clean } = fields;
36
36
  return { clean, password: password };
37
37
  }
38
- /** ConnRecord → 用户可见的 ConnectionMeta(契约见 lib/adapters/types.ts) */
39
- function toMeta(rec) {
38
+ /** ConnRecord → 用户可见的 ConnectionMeta(契约见 lib/adapters/types.ts)。
39
+ * hasPassword 只回布尔指示,明文绝不出库。 */
40
+ function toMeta(rec, hasPassword = false) {
40
41
  const meta = { id: rec.id, kind: rec.kind };
41
42
  if (rec.name !== undefined)
42
43
  meta.name = rec.name;
43
- if (rec.urlSafe !== undefined)
44
+ if (rec.urlSafe !== undefined) {
44
45
  meta.safeUrl = rec.urlSafe;
46
+ meta.mode = 'url';
47
+ }
48
+ else {
49
+ meta.mode = 'fields';
50
+ }
51
+ if (hasPassword)
52
+ meta.hasPassword = true;
45
53
  if (rec.fields) {
46
- const { host, port, database } = rec.fields;
54
+ const { host, port, user, database } = rec.fields;
47
55
  if (typeof host === 'string')
48
56
  meta.host = host;
49
57
  if (typeof port === 'number')
50
58
  meta.port = port;
59
+ if (typeof user === 'string')
60
+ meta.user = user;
51
61
  if (typeof database === 'string')
52
62
  meta.database = database;
53
63
  }
@@ -71,12 +81,16 @@ export class ConnectionStore {
71
81
  findRec(data, id) {
72
82
  return data.connections.find((c) => c.id === id);
73
83
  }
84
+ /** 该连接是否已存密码(布尔指示,不读明文) */
85
+ hasPassword(id) {
86
+ return this.secrets.get(id)?.password !== undefined;
87
+ }
74
88
  list() {
75
- return this.load().connections.map(toMeta);
89
+ return this.load().connections.map((r) => toMeta(r, this.hasPassword(r.id)));
76
90
  }
77
91
  get(id) {
78
92
  const rec = this.findRec(this.load(), id);
79
- return rec ? toMeta(rec) : undefined;
93
+ return rec ? toMeta(rec, this.hasPassword(id)) : undefined;
80
94
  }
81
95
  /** 新建连接;id 已存在时抛错 */
82
96
  create(input) {
@@ -103,7 +117,7 @@ export class ConnectionStore {
103
117
  this.secrets.set(input.id, secretPatch);
104
118
  data.connections.push(rec);
105
119
  this.save(data);
106
- return toMeta(rec);
120
+ return toMeta(rec, this.hasPassword(input.id));
107
121
  }
108
122
  /** 部分更新;不存在返回 undefined。url/fields 变更时同步拆分 secrets */
109
123
  update(id, patch) {
@@ -125,8 +139,26 @@ export class ConnectionStore {
125
139
  rec.urlSafe = redactUrl(patch.url);
126
140
  this.secrets.set(id, { url: patch.url });
127
141
  }
142
+ if (patch.clearUrl && rec.urlSafe !== undefined) {
143
+ delete rec.urlSafe;
144
+ // 密码迁移:原 url 内嵌密码 → fields 密码(切方式后保持可连,编辑留空即保留)
145
+ const secUrl = this.secrets.get(id)?.url;
146
+ if (secUrl !== undefined) {
147
+ try {
148
+ const u = new URL(secUrl);
149
+ if (u.password && this.secrets.get(id)?.password === undefined) {
150
+ this.secrets.set(id, { password: decodeURIComponent(u.password) });
151
+ }
152
+ }
153
+ catch {
154
+ /* 非 URL 形态,忽略迁移 */
155
+ }
156
+ }
157
+ // set 为合并写;显式置 undefined 经 JSON 序列化后等效删除该键
158
+ this.secrets.set(id, { url: undefined });
159
+ }
128
160
  this.save(data);
129
- return toMeta(rec);
161
+ return toMeta(rec, this.hasPassword(id));
130
162
  }
131
163
  /** 删除连接,级联删除 secrets 与该连接的所有项目授权。
132
164
  * 顺序按"失败开放"原则:先删权限(grants),再删机密(secrets),最后改
@@ -150,7 +182,7 @@ export class ConnectionStore {
150
182
  const rec = this.findRec(this.load(), id);
151
183
  if (!rec)
152
184
  throw new Error(`连接不存在: ${id}`);
153
- const meta = toMeta(rec);
185
+ const meta = toMeta(rec, this.secrets.get(id)?.password !== undefined);
154
186
  const rc = { meta };
155
187
  const sec = this.secrets.get(id);
156
188
  if (sec?.url !== undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-db-tool",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
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",
@@ -33,6 +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
37
  - 返回 `{columns, rows, rowCount, truncated?}`,所有单元格已规范化为 string/number/null。
37
38
  - ro 与 rw 授权下都可用,但只读接口收到非查询语句会被拒。
38
39
 
@@ -43,6 +44,7 @@ DatabaseManager({ action: "execute", conn_id: "c1", statement: "UPDATE users SET
43
44
  ```
44
45
 
45
46
  - `statement`(或 `sql`)必填,单条语句;DDL/DML/维护命令都会触发危险确认。
47
+ - 可选 `database`:跨库执行目标,语义同 query(pg/gaussdb 支持按库路由,目标库权限由数据库侧用户权限控制)。
46
48
  - 返回 `{affectedRows?, message}`,`message` 为中文结果描述。
47
49
  - ro 授权下直接返回 `READ_ONLY` 错误。
48
50