dsh-db-tool 0.1.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.
Files changed (41) hide show
  1. package/README.md +66 -0
  2. package/client/README.md +53 -0
  3. package/client/client.js +1128 -0
  4. package/cordis.patch.yml +6 -0
  5. package/dist/adapters/dmdb/index.js +326 -0
  6. package/dist/adapters/gaussdb/index.js +32 -0
  7. package/dist/adapters/index.js +41 -0
  8. package/dist/adapters/mongodb/index.js +508 -0
  9. package/dist/adapters/mysql/index.js +222 -0
  10. package/dist/adapters/oracle/index.js +369 -0
  11. package/dist/adapters/postgresql/index.js +15 -0
  12. package/dist/adapters/redis/index.js +492 -0
  13. package/dist/adapters/sql-shared/common.js +69 -0
  14. package/dist/adapters/sql-shared/pg-like.js +170 -0
  15. package/dist/adapters/sqlite/index.js +106 -0
  16. package/dist/adapters/types.js +1 -0
  17. package/dist/guard/index.js +221 -0
  18. package/dist/http/index.js +396 -0
  19. package/dist/index.js +130 -0
  20. package/dist/manager.js +469 -0
  21. package/dist/script/runner.js +147 -0
  22. package/dist/script/worker.cjs +104 -0
  23. package/dist/store/audit.js +41 -0
  24. package/dist/store/connections.js +167 -0
  25. package/dist/store/grants.js +82 -0
  26. package/dist/store/index.js +41 -0
  27. package/dist/store/io.js +66 -0
  28. package/dist/store/normalize.js +22 -0
  29. package/dist/store/secrets.js +42 -0
  30. package/docs/api-contract.md +76 -0
  31. package/docs/apple-redesign-spec.md +80 -0
  32. package/docs/dsh-market-submission/mengqi1436__dsh-db-tool.yml +6 -0
  33. package/docs/install.md +75 -0
  34. package/docs/review-findings.md +95 -0
  35. package/docs/skill.md +48 -0
  36. package/package.json +70 -0
  37. package/scripts/build-gaussdb.ps1 +72 -0
  38. package/scripts/build-gaussdb.sh +53 -0
  39. package/scripts/diag-profile.mjs +21 -0
  40. package/scripts/verify-host.mjs +53 -0
  41. package/skills/db-admin/SKILL.md +138 -0
@@ -0,0 +1,222 @@
1
+ /**
2
+ * MySQL 适配器(mysql2/promise)。
3
+ * query=读用 pool.query();execute=写用 pool.execute()(真 prepare,占位符 ?)。
4
+ * DECIMAL 官方默认 string 保持;dateStrings:true 使 DATE/DATETIME 直接输出字符串。
5
+ * ro 模式:multipleStatements 固定 false(防堆叠注入);建连后会话级只读(SET SESSION
6
+ * TRANSACTION READ ONLY)+ query 首词只读白名单双保险;服务层拦截 execute。
7
+ */
8
+ import mysql from 'mysql2/promise';
9
+ import { assertIdent, clampLimit, clampOffset, humanize, normalizeCell, quoteIdent } from '../sql-shared/common.js';
10
+ /** ro 模式下 query 允许的读语句首词白名单 */
11
+ const RO_READ_PREFIX = /^(select|show|desc|describe|explain|use|help|table)\b/i;
12
+ function connOptions(conn) {
13
+ const base = {
14
+ waitForConnections: true,
15
+ connectionLimit: 10,
16
+ maxIdle: 10,
17
+ idleTimeout: 60000,
18
+ queueLimit: 0,
19
+ enableKeepAlive: true,
20
+ keepAliveInitialDelay: 0,
21
+ multipleStatements: false,
22
+ dateStrings: true,
23
+ connectTimeout: 10000,
24
+ charset: 'utf8mb4_general_ci',
25
+ };
26
+ if (conn.url) {
27
+ const u = new URL(conn.url);
28
+ base.host = u.hostname;
29
+ base.port = Number(u.port) || 3306;
30
+ try {
31
+ if (u.username)
32
+ base.user = decodeURIComponent(u.username);
33
+ if (u.password)
34
+ base.password = decodeURIComponent(u.password);
35
+ }
36
+ catch {
37
+ throw new Error('mysql 连接 URL 的用户名或密码 percent 编码非法(如含未编码的 %),请 URL 编码后重试');
38
+ }
39
+ const db = u.pathname.replace(/^\//, '');
40
+ if (db)
41
+ base.database = db;
42
+ const ssl = u.searchParams.get('ssl');
43
+ if (conn.ssl || ssl === 'true' || ssl === 'require')
44
+ base.ssl = { rejectUnauthorized: false };
45
+ }
46
+ else if (conn.fields) {
47
+ const f = conn.fields;
48
+ base.host = String(f.host ?? '127.0.0.1');
49
+ base.port = Number(f.port ?? 3306);
50
+ base.user = String(f.user ?? f.username ?? 'root');
51
+ if (f.password != null)
52
+ base.password = String(f.password);
53
+ if (f.database != null)
54
+ base.database = String(f.database);
55
+ if (conn.ssl)
56
+ base.ssl = { rejectUnauthorized: false };
57
+ }
58
+ else {
59
+ throw new Error('mysql 连接缺少 url 或 fields 配置');
60
+ }
61
+ return base;
62
+ }
63
+ export async function createMysqlAdapter(conn, opts) {
64
+ const pool = mysql.createPool(connOptions(conn));
65
+ const readOnly = opts?.mode === 'ro';
66
+ if (readOnly) {
67
+ // 服务器级 ro 强制:每个新底层连接自动设为只读会话(应用层另有 query 白名单双保险)。
68
+ // 运行时连接可能是 promise 包装或 callback 版:query() 返回 promise 才需要吞掉 SET 失败。
69
+ pool.on('connection', (c) => {
70
+ const r = c.query('SET SESSION TRANSACTION READ ONLY');
71
+ if (r && typeof r.catch === 'function') {
72
+ void r.catch(() => { });
73
+ }
74
+ });
75
+ }
76
+ /** 事务激活期间路由到同一连接,保证 BEGIN/COMMIT 生效 */
77
+ let txConn = null;
78
+ // PoolConnection 与 Pool 的联合会让 mysql2 的 query/execute 重载解析失败,收窄为 Pool 视图
79
+ const runner = () => (txConn ?? pool);
80
+ function toQueryResult(rows, fields) {
81
+ const columns = fields.map((f) => String(f.name));
82
+ return {
83
+ columns,
84
+ rows: rows.map((r) => columns.map((c) => normalizeCell(r[c]))),
85
+ rowCount: rows.length,
86
+ };
87
+ }
88
+ const tx = {
89
+ begin: () => humanize('mysql 事务开启', async () => {
90
+ if (txConn)
91
+ throw new Error('事务已开启,请勿重复 begin');
92
+ txConn = await pool.getConnection();
93
+ try {
94
+ await txConn.beginTransaction();
95
+ }
96
+ catch (e) {
97
+ txConn.release();
98
+ txConn = null;
99
+ throw e;
100
+ }
101
+ }),
102
+ commit: () => humanize('mysql 事务提交', async () => {
103
+ const c = txConn;
104
+ if (!c)
105
+ throw new Error('没有进行中的事务');
106
+ txConn = null;
107
+ try {
108
+ await c.commit();
109
+ }
110
+ finally {
111
+ c.release();
112
+ }
113
+ }),
114
+ rollback: () => humanize('mysql 事务回滚', async () => {
115
+ const c = txConn;
116
+ if (!c)
117
+ throw new Error('没有进行中的事务');
118
+ txConn = null;
119
+ try {
120
+ await c.rollback();
121
+ }
122
+ finally {
123
+ c.release();
124
+ }
125
+ }),
126
+ };
127
+ const defaultDb = () => {
128
+ let db = conn.fields?.database != null ? String(conn.fields.database) : '';
129
+ if (!db && conn.url) {
130
+ // URL 配置:默认库取 pathname 首段(与 connOptions 的解析一致)
131
+ const p = new URL(conn.url).pathname.slice(1);
132
+ if (p)
133
+ db = p;
134
+ }
135
+ if (!db)
136
+ throw new Error('未指定数据库:请传入 database 参数或配置默认库');
137
+ // fields/URL 提供的库名统一过标识符校验(防注入)
138
+ return assertIdent(db, '库名');
139
+ };
140
+ return {
141
+ kind: 'mysql',
142
+ connId: conn.meta.id,
143
+ tx,
144
+ testConnect: () => humanize('mysql 连接测试', async () => {
145
+ const [rows] = await pool.query('SELECT VERSION() AS v');
146
+ return { ok: true, serverInfo: `MySQL ${String(rows[0]?.v ?? '')}` };
147
+ }),
148
+ query: (sql, params) => humanize('mysql 查询失败', async () => {
149
+ if (readOnly && !RO_READ_PREFIX.test(sql.trimStart())) {
150
+ throw new Error('DRIVER_ERROR: 只读(ro)模式下 query 仅允许读语句(select/show/desc/describe/explain/use/help/table),写操作请使用可写连接');
151
+ }
152
+ const [rows, fields] = await runner().query(sql, params ?? []);
153
+ if (!Array.isArray(rows))
154
+ return { columns: [], rows: [], rowCount: 0 };
155
+ return toQueryResult(rows, fields);
156
+ }),
157
+ execute: (statement, params) => humanize('mysql 执行失败', async () => {
158
+ // mysql2 的 ExecuteValues 不接受 unknown[],此处值均为绑定参数(string|number|null)
159
+ const [result] = await runner().execute(statement, (params ?? []));
160
+ const header = (Array.isArray(result) ? result[0] : result);
161
+ const n = header?.affectedRows;
162
+ return {
163
+ affectedRows: typeof n === 'number' ? n : undefined,
164
+ message: typeof n === 'number'
165
+ ? `执行成功,受影响 ${n} 行`
166
+ : '执行成功',
167
+ };
168
+ }),
169
+ listDatabases: () => humanize('mysql 列出数据库', async () => {
170
+ const [rows] = await pool.query('SELECT SCHEMA_NAME FROM information_schema.SCHEMATA ORDER BY SCHEMA_NAME');
171
+ return rows.map((r) => String(r.SCHEMA_NAME));
172
+ }),
173
+ listTables: (database) => humanize('mysql 列出表', async () => {
174
+ const db = database ? assertIdent(database, '库名') : defaultDb();
175
+ const [rows] = await pool.query(`SELECT TABLE_NAME, TABLE_TYPE, TABLE_COMMENT FROM information_schema.TABLES
176
+ WHERE TABLE_SCHEMA = ? ORDER BY TABLE_NAME`, [db]);
177
+ return rows.map((r) => {
178
+ const rawType = String(r.TABLE_TYPE);
179
+ const comment = r.TABLE_COMMENT;
180
+ return {
181
+ name: String(r.TABLE_NAME),
182
+ type: rawType === 'BASE TABLE' ? 'TABLE' : rawType,
183
+ comment: comment != null && comment !== '' ? String(comment) : undefined,
184
+ database: db,
185
+ };
186
+ });
187
+ }),
188
+ describeTable: (table, database) => humanize('mysql 查看表结构', async () => {
189
+ const db = database ? assertIdent(database, '库名') : defaultDb();
190
+ const tbl = assertIdent(table, '表名');
191
+ const [rows] = await pool.query(`SELECT COLUMN_NAME, COLUMN_TYPE, IS_NULLABLE, COLUMN_KEY, COLUMN_DEFAULT, COLUMN_COMMENT
192
+ FROM information_schema.COLUMNS
193
+ WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? ORDER BY ORDINAL_POSITION`, [db, tbl]);
194
+ if (rows.length === 0)
195
+ throw new Error(`表不存在: ${db}.${tbl}`);
196
+ return rows.map((r) => {
197
+ const dflt = r.COLUMN_DEFAULT;
198
+ const comment = r.COLUMN_COMMENT;
199
+ return {
200
+ name: String(r.COLUMN_NAME),
201
+ dataType: String(r.COLUMN_TYPE),
202
+ nullable: r.IS_NULLABLE === 'YES',
203
+ key: r.COLUMN_KEY ? String(r.COLUMN_KEY) : undefined,
204
+ default: dflt == null ? null : String(dflt),
205
+ comment: comment != null && comment !== '' ? String(comment) : undefined,
206
+ };
207
+ });
208
+ }),
209
+ previewRows: (table, limit, database, offset) => humanize('mysql 预览行', async () => {
210
+ const db = quoteIdent(database ? assertIdent(database, '库名') : defaultDb(), '`');
211
+ const tbl = quoteIdent(assertIdent(table, '表名'), '`');
212
+ const lim = clampLimit(limit);
213
+ const off = clampOffset(offset);
214
+ const [rows, fields] = await pool.query(`SELECT * FROM ${db}.${tbl} LIMIT ? OFFSET ?`, [lim, off]);
215
+ if (!Array.isArray(rows))
216
+ return { columns: [], rows: [], rowCount: 0 };
217
+ return toQueryResult(rows, fields);
218
+ }),
219
+ close: () => humanize('mysql 关闭连接', () => pool.end()),
220
+ };
221
+ }
222
+ export const factory = (conn) => createMysqlAdapter(conn);
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Oracle 适配器(oracledb v7 thin 模式,DB 12.1+)。
3
+ *
4
+ * 设计要点(依据官方文档调研):
5
+ * - oracledb.createPool({ user, password, connectString, poolMin: 1, poolMax: 10, poolTimeout: 60 })。
6
+ * 禁止 SYSDBA 等特权连接(不透传 privilege)。
7
+ * - CLOB 经全局 fetchAsString 转字符串;BLOB 经 execute 的 fetchInfo 转 string。
8
+ * NUMBER 保持 number(超精度损失由用户承担,见文档标注)。
9
+ * - query() 仅允许 SELECT/WITH;execute() DML/DDL,autoCommit: true 单语句语义。
10
+ * - Oracle 无 BEGIN,事务用 SET TRANSACTION READ WRITE + commit/rollback(tx 成员)。
11
+ * - DDL 隐式提交:execute 对 DDL 的 message 附注「DDL 已隐式提交,不可回滚」。
12
+ * - 标识符默认大写存储:listTables/describeTable 内部统一 toUpperCase 匹配。
13
+ * - 锁号/密码验证器等常见连接错误转人类可读提示(见 ORA_HINTS)。
14
+ */
15
+ import oracledb from 'oracledb';
16
+ // CLOB 统一按字符串返回(BLOB 不能走 fetchAsString,须用 execute 的 fetchInfo)
17
+ if (!oracledb.fetchAsString.includes(oracledb.CLOB)) {
18
+ oracledb.fetchAsString = [...oracledb.fetchAsString, oracledb.CLOB];
19
+ }
20
+ /** BLOB → string(execute 级选项) */
21
+ const BLOB_FETCH = {
22
+ fetchInfo: { BLOB: { type: oracledb.STRING } },
23
+ };
24
+ const ROWS_MAX = 500;
25
+ const CELL_TRUNC = 1000;
26
+ const DDL_KEYWORDS = new Set([
27
+ 'CREATE', 'ALTER', 'DROP', 'TRUNCATE', 'RENAME', 'GRANT', 'REVOKE',
28
+ 'COMMENT', 'ANALYZE', 'AUDIT', 'NOAUDIT', 'FLASHBACK', 'PURGE',
29
+ ]);
30
+ /** 常见 ORA 错误 → 人类可读提示 */
31
+ export const ORA_HINTS = [
32
+ [/ORA-01017/, '用户名或密码错误'],
33
+ [/ORA-12154/, '无法解析连接串中的服务名,请检查 connectString / service_name'],
34
+ [/ORA-12505/, '监听器不认识该 SID:请使用服务名(service_name)而非 SID'],
35
+ [/ORA-12514/, '监听器当前未注册该服务名,请检查服务名拼写与数据库注册状态'],
36
+ [/ORA-12541/, '无法连接监听器,请检查主机与端口'],
37
+ [/ORA-12543/, '网络无法到达目标主机,请检查主机/端口/防火墙'],
38
+ [/ORA-12560/, 'TNS 协议适配器错误,请检查连接串格式(应为 host:port/service_name)'],
39
+ [/ORA-28040/, '客户端与服务器密码验证器版本不兼容(服务器 SQLNET.ALLOWED_LOGON_VERSION_SERVER 需放宽)'],
40
+ [/ORA-28000/, '账号已被锁定,请联系 DBA 解锁'],
41
+ [/ORA-28001/, '密码已过期,请联系 DBA 重置'],
42
+ [/ORA-00942/, '表或视图不存在(Oracle 标识符默认大写存储,注意大小写与 owner)'],
43
+ [/ORA-00904/, '列名无效(Oracle 标识符默认大写存储,注意大小写)'],
44
+ ];
45
+ export function humanizeOraError(e) {
46
+ const msg = e instanceof Error ? e.message : String(e);
47
+ const hint = ORA_HINTS.find(([re]) => re.test(msg))?.[1];
48
+ return new Error(hint ? `Oracle 错误:${msg}(提示:${hint})` : `Oracle 错误:${msg}`);
49
+ }
50
+ /** 标识符基本校验(引用创建的 Oracle 名字可含任意字符,白名单会误杀):
51
+ * 非空、≤128、无 NUL/换行。防注入靠数据字典参数绑定 + preview 处 escIdent 转义。
52
+ * 保留原名不强制大写:describeTable 的 SQL 用 UPPER(:tab) 兜底匹配未引用建表。 */
53
+ export function sanitizeIdentifier(name, label) {
54
+ if (typeof name !== 'string' || name.length === 0 || name.length > 128 ||
55
+ /[\0\r\n]/.test(name)) {
56
+ throw new Error(`非法 Oracle ${label}「${name}」:不允许为空、超 128 字符或含 NUL/换行`);
57
+ }
58
+ return name;
59
+ }
60
+ /** 双引号标识符转义(" → ""),拼接进 SQL 前必须过此函数 */
61
+ function escIdent(name) {
62
+ return name.replaceAll('"', '""');
63
+ }
64
+ function trunc(s, max = CELL_TRUNC) {
65
+ return s.length > max ? `${s.slice(0, max)}…[截断,共${s.length}字符]` : s;
66
+ }
67
+ /** 行值规范化为 NormalizedCell(Lob 兜底转字符串并截断) */
68
+ async function normalizeCell(v) {
69
+ if (v === null || v === undefined)
70
+ return null;
71
+ if (typeof v === 'number')
72
+ return Number.isFinite(v) ? v : String(v);
73
+ if (typeof v === 'string')
74
+ return trunc(v);
75
+ if (v instanceof Date)
76
+ return v.toISOString();
77
+ if (typeof v === 'object' && v.constructor?.name === 'Lob') {
78
+ const s = await v.toString();
79
+ return trunc(s);
80
+ }
81
+ return trunc(String(v));
82
+ }
83
+ /** 连接参数解析:url(oracle://user:pass@host:1521/service)或 fields */
84
+ export function resolveOracleConn(conn) {
85
+ if (conn.url) {
86
+ const u = new URL(conn.url);
87
+ const service = u.pathname.replace(/^\//, '');
88
+ if (!u.hostname || service === '') {
89
+ throw new Error('Oracle URL 格式应为 oracle://user:pass@host:1521/SERVICE_NAME');
90
+ }
91
+ const user = decodeURIComponent(u.username);
92
+ if (user === '') {
93
+ throw new Error('Oracle URL 缺少用户名:应为 oracle://user:pass@host:1521/SERVICE_NAME');
94
+ }
95
+ return {
96
+ user,
97
+ password: decodeURIComponent(u.password),
98
+ connectString: `${u.hostname}:${u.port || 1521}/${service}`,
99
+ };
100
+ }
101
+ const f = conn.fields ?? {};
102
+ const host = typeof f.host === 'string' ? f.host : '';
103
+ const port = typeof f.port === 'number' ? f.port : 1521;
104
+ const service = typeof f.service === 'string' ? f.service : typeof f.database === 'string' ? f.database : '';
105
+ if (host === '' || service === '') {
106
+ throw new Error('Oracle 连接需要 url(oracle://user:pass@host:1521/SERVICE)或 fields(host/port/service)');
107
+ }
108
+ const user = typeof f.user === 'string' ? f.user : '';
109
+ if (user === '') {
110
+ throw new Error('Oracle 连接缺少用户名(fields.user)');
111
+ }
112
+ return {
113
+ user,
114
+ password: typeof f.password === 'string' ? f.password : '',
115
+ connectString: `${host}:${port}/${service}`,
116
+ };
117
+ }
118
+ export async function createOracleAdapter(conn, opts) {
119
+ const mode = opts?.mode ?? 'rw';
120
+ let pool;
121
+ if (opts?.pool) {
122
+ pool = opts.pool;
123
+ }
124
+ else {
125
+ const { user, password, connectString } = resolveOracleConn(conn);
126
+ try {
127
+ pool = await oracledb.createPool({
128
+ user,
129
+ password,
130
+ connectString,
131
+ poolMin: 1,
132
+ poolMax: 10,
133
+ poolTimeout: 60,
134
+ });
135
+ }
136
+ catch (e) {
137
+ throw humanizeOraError(e);
138
+ }
139
+ }
140
+ function requireRw(action) {
141
+ if (mode === 'ro') {
142
+ throw new Error(`连接为只读(ro)模式,拒绝执行${action};请使用 rw 连接或只发查询(query)`);
143
+ }
144
+ }
145
+ function isDdl(sql) {
146
+ const first = sql.trim().split(/\s+/)[0]?.toUpperCase() ?? '';
147
+ return DDL_KEYWORDS.has(first);
148
+ }
149
+ async function toExecResult(sql, r) {
150
+ const ddlNote = isDdl(sql) ? '(DDL 已隐式提交,不可回滚)' : '';
151
+ return {
152
+ affectedRows: typeof r.rowsAffected === 'number' && r.rowsAffected > 0 ? r.rowsAffected : undefined,
153
+ message: `${sql.trim().split(/\s+/)[0]?.toUpperCase() ?? 'SQL'} 完成${r.rowsAffected ? `,影响 ${r.rowsAffected} 行` : ''}${ddlNote}`,
154
+ };
155
+ }
156
+ // autoCommit 默认 true:query/元数据 SELECT 结束隐式事务避免 ro 事务悬挂;tx 专用连接显式 autoCommit:false 覆盖
157
+ const execOpts = { outFormat: oracledb.OBJECT, maxRows: ROWS_MAX + 1, autoCommit: true, ...BLOB_FETCH };
158
+ /**
159
+ * 从池借一条连接执行后归还。⚠️ oracledb 的 Pool 没有 execute,
160
+ * 一切语句必须在 Connection 上执行;close() 即归还连接。
161
+ */
162
+ async function withConn(fn) {
163
+ const c = await pool.getConnection();
164
+ try {
165
+ return await fn(c);
166
+ }
167
+ finally {
168
+ await c.close().catch(() => { });
169
+ }
170
+ }
171
+ // 注意:currentUser / 系统 schema 名单在 adapter 对象之前声明,
172
+ // 不再依赖「方法仅运行期调用」的隐式 TDZ 保证(审查项:脆弱模式消除)
173
+ const currentUser = resolveOracleConn(conn).user.toUpperCase();
174
+ /** Oracle 内建系统 schema(核心名单 + APEX/flows 组件前缀):业务不可查询,列表中过滤 */
175
+ const ORA_SYSTEM_SCHEMAS = new Set([
176
+ 'SYS', 'SYSTEM', 'OUTLN', 'XDB', 'CTXSYS', 'MDSYS', 'OLAPSYS', 'ORDDATA', 'ORDSYS',
177
+ 'WMSYS', 'DBSNMP', 'APPQOSSYS', 'AUDSYS', 'LBACSYS', 'DVSYS', 'OJVMSYS', 'DBSFWUSER',
178
+ 'GSMADMIN_INTERNAL', 'GSMCATUSER', 'GGSYS', 'REMOTE_SCHEDULER_AGENT', 'ANONYMOUS',
179
+ ]);
180
+ const isOraSystemSchema = (o) => ORA_SYSTEM_SCHEMAS.has(o) || /^(APEX|FLOWS)_/.test(o);
181
+ /** owner 解析:database 参数或默认当前用户(保留原样——引用创建的 schema 大小写敏感) */
182
+ function ownerOf(database) {
183
+ return sanitizeIdentifier(database ?? currentUser, 'schema/owner 名');
184
+ }
185
+ const adapter = {
186
+ kind: 'oracle',
187
+ connId: conn.meta.id,
188
+ async testConnect() {
189
+ try {
190
+ const r = await withConn((c) => c.execute('SELECT banner FROM v$version WHERE ROWNUM = 1', [], execOpts));
191
+ const row = r.rows?.[0];
192
+ const banner = row ? String(Object.values(row)[0] ?? '') : '';
193
+ return { ok: true, serverInfo: banner };
194
+ }
195
+ catch {
196
+ // v$version 在部分受限环境对普通用户不可读(ORA-00942/ORA-01031):
197
+ // 版本信息尽力而为,连通性本身用 dual 兜底判定,避免误报连接失败
198
+ try {
199
+ await withConn((c) => c.execute('SELECT 1 FROM dual', [], execOpts));
200
+ return { ok: true, serverInfo: 'Oracle(版本信息不可读:v$version 无权限)' };
201
+ }
202
+ catch (e) {
203
+ return { ok: false, error: humanizeOraError(e).message };
204
+ }
205
+ }
206
+ },
207
+ async query(sql, params) {
208
+ const head = sql.trim().split(/\s+/)[0]?.toUpperCase() ?? '';
209
+ if (head !== 'SELECT' && head !== 'WITH') {
210
+ throw new Error('Oracle query 仅允许 SELECT/WITH 语句;写操作或 DDL 请走 execute');
211
+ }
212
+ try {
213
+ const r = await withConn((c) => c.execute(sql, params ?? [], execOpts));
214
+ const allRows = r.rows ?? [];
215
+ const truncated = allRows.length > ROWS_MAX;
216
+ const rows = allRows.slice(0, ROWS_MAX);
217
+ const columns = (r.metaData ?? []).map((m) => m.name);
218
+ const normRows = [];
219
+ for (const row of rows) {
220
+ normRows.push(await Promise.all(Object.values(row).map(normalizeCell)));
221
+ }
222
+ return { columns, rows: normRows, rowCount: normRows.length, truncated: truncated || undefined };
223
+ }
224
+ catch (e) {
225
+ throw humanizeOraError(e);
226
+ }
227
+ },
228
+ async execute(statement, params) {
229
+ requireRw('execute');
230
+ try {
231
+ const r = await withConn((c) => c.execute(statement, params ?? [], execOpts));
232
+ return await toExecResult(statement, r);
233
+ }
234
+ catch (e) {
235
+ throw humanizeOraError(e);
236
+ }
237
+ },
238
+ /**
239
+ * 事务:从池取专用连接执行多条语句,成功 commit、异常 rollback。
240
+ * Oracle 无 BEGIN,用 SET TRANSACTION READ WRITE 开始显式事务。
241
+ */
242
+ async tx(fn) {
243
+ requireRw('事务(tx)');
244
+ const c = await pool.getConnection();
245
+ try {
246
+ await c.execute('SET TRANSACTION READ WRITE');
247
+ const exec = async (sql, binds) => {
248
+ const r = await c.execute(sql, binds ?? [], { ...execOpts, autoCommit: false });
249
+ return toExecResult(sql, r);
250
+ };
251
+ const out = await fn(exec);
252
+ await c.commit();
253
+ return out;
254
+ }
255
+ catch (e) {
256
+ try {
257
+ await c.rollback();
258
+ }
259
+ catch { /* 已断开等场景忽略 */ }
260
+ throw humanizeOraError(e);
261
+ }
262
+ finally {
263
+ try {
264
+ await c.close();
265
+ }
266
+ catch { /* 归还失败忽略 */ }
267
+ }
268
+ },
269
+ async listDatabases() {
270
+ // Oracle 为单库多 schema 模型:「数据库」下拉列出可浏览的 schema(有表者),
271
+ // 失败回退当前用户 schema,绝不用 service 名等占位值(会污染 owner 参数)。
272
+ // 内建系统 schema(数据字典/组件账户)业务不可查询,列出只会让展开时报
273
+ // ORA-00942/ORA-01031 类错误,故过滤(保留当前用户自身)。
274
+ try {
275
+ const r = await withConn((c) => c.execute('SELECT DISTINCT owner FROM all_tables ORDER BY owner', [], execOpts));
276
+ const owners = (r.rows ?? [])
277
+ .map((row) => String(row.OWNER ?? '').trim())
278
+ .filter(Boolean)
279
+ .filter((o) => o === currentUser || !isOraSystemSchema(o.toUpperCase()));
280
+ return owners.length > 0 ? owners : [currentUser];
281
+ }
282
+ catch {
283
+ return [currentUser];
284
+ }
285
+ },
286
+ async listTables(database) {
287
+ const owner = ownerOf(database);
288
+ try {
289
+ const r = await withConn((c) => c.execute('SELECT table_name FROM all_tables WHERE owner = :owner ORDER BY table_name FETCH FIRST 500 ROWS ONLY', [owner], execOpts));
290
+ const rows = r.rows ?? [];
291
+ return rows.map((row) => ({ name: String(row.TABLE_NAME ?? ''), type: 'TABLE' }));
292
+ }
293
+ catch (e) {
294
+ throw humanizeOraError(e);
295
+ }
296
+ },
297
+ async describeTable(table, database) {
298
+ const owner = ownerOf(database);
299
+ // 原名精确匹配优先;未引用创建的表名在字典中为大写存储,UPPER 兜底
300
+ const tab = sanitizeIdentifier(table, '表名');
301
+ try {
302
+ // 同连接内两查保证一致性
303
+ const { colsR, pkR } = await withConn(async (c) => ({
304
+ colsR: await c.execute(`SELECT column_name, data_type, data_length, data_precision, data_scale, nullable, data_default
305
+ FROM all_tab_columns WHERE owner = :owner AND (table_name = :tab OR table_name = UPPER(:tab)) ORDER BY column_id`, [owner, tab], execOpts),
306
+ pkR: await c.execute(`SELECT cols.column_name
307
+ FROM all_constraints cons
308
+ JOIN all_cons_columns cols
309
+ ON cons.owner = cols.owner AND cons.constraint_name = cols.constraint_name
310
+ WHERE cons.constraint_type = 'P' AND cons.owner = :owner AND (cons.table_name = :tab OR cons.table_name = UPPER(:tab))`, [owner, tab], execOpts),
311
+ }));
312
+ const pkSet = new Set((pkR.rows ?? []).map((r2) => String(r2.COLUMN_NAME ?? '')));
313
+ return (colsR.rows ?? []).map((row) => {
314
+ const dataType = String(row.DATA_TYPE ?? '');
315
+ const precision = row.DATA_PRECISION;
316
+ const scale = row.DATA_SCALE;
317
+ const length = row.DATA_LENGTH;
318
+ let typeStr = dataType;
319
+ if (precision !== null && precision !== undefined) {
320
+ typeStr = scale !== null && scale !== undefined && Number(scale) > 0
321
+ ? `${dataType}(${Number(precision)},${Number(scale)})`
322
+ : `${dataType}(${Number(precision)})`;
323
+ }
324
+ else if (/^(VARCHAR|NVARCHAR|CHAR|NCHAR|RAW)/.test(dataType) && length !== null && length !== undefined) {
325
+ typeStr = `${dataType}(${Number(length)})`;
326
+ }
327
+ const def = row.DATA_DEFAULT;
328
+ return {
329
+ name: String(row.COLUMN_NAME ?? ''),
330
+ dataType: typeStr,
331
+ nullable: String(row.NULLABLE ?? 'Y') === 'Y',
332
+ ...(pkSet.has(String(row.COLUMN_NAME ?? '')) ? { key: 'PRI' } : {}),
333
+ default: def === null || def === undefined ? null : trunc(String(def).trim()),
334
+ };
335
+ });
336
+ }
337
+ catch (e) {
338
+ throw humanizeOraError(e);
339
+ }
340
+ },
341
+ async previewRows(table, limit, database, offset) {
342
+ const owner = ownerOf(database);
343
+ const tab = sanitizeIdentifier(table, '表名'); // 拼接前经 escIdent 转义
344
+ const n = Math.max(1, Math.min(Math.floor(limit) || 20, ROWS_MAX));
345
+ const off = Math.max(0, Math.floor(offset ?? 0) || 0);
346
+ try {
347
+ const r = await withConn((c) => c.execute(`SELECT * FROM "${escIdent(owner)}"."${escIdent(tab)}" OFFSET :o ROWS FETCH NEXT :n ROWS ONLY`, [off, n], execOpts));
348
+ const rows = r.rows ?? [];
349
+ const columns = (r.metaData ?? []).map((m) => m.name);
350
+ const normRows = [];
351
+ for (const row of rows) {
352
+ normRows.push(await Promise.all(Object.values(row).map(normalizeCell)));
353
+ }
354
+ return { columns, rows: normRows, rowCount: normRows.length };
355
+ }
356
+ catch (e) {
357
+ throw humanizeOraError(e);
358
+ }
359
+ },
360
+ async close() {
361
+ try {
362
+ await pool.close();
363
+ }
364
+ catch { /* 已关闭忽略 */ }
365
+ },
366
+ };
367
+ return adapter;
368
+ }
369
+ export const factory = async (conn) => createOracleAdapter(conn);
@@ -0,0 +1,15 @@
1
+ /**
2
+ * PostgreSQL 适配器(pg 8.x)。核心逻辑在 sql-shared/pg-like.ts,此处仅做驱动绑定。
3
+ * ro 模式:pool 'connect' 事件设只读会话(服务器级)+ 服务层拦截 execute(双保险)。
4
+ *
5
+ * 说明:pg 包未提供 ESM 类型入口(exports 无 types 条件),统一用 createRequire
6
+ * 加载 CJS 入口并按 PgLikeDriver 结构使用——与 gaussdb vendor 加载路径对称。
7
+ */
8
+ import { createRequire } from 'node:module';
9
+ import { createPgLikeAdapter } from '../sql-shared/pg-like.js';
10
+ const require = createRequire(import.meta.url);
11
+ export async function createPostgresqlAdapter(conn, opts) {
12
+ const driver = require('pg');
13
+ return createPgLikeAdapter('postgresql', driver, conn, opts);
14
+ }
15
+ export const factory = (conn) => createPostgresqlAdapter(conn);