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.
- package/README.md +66 -0
- package/client/README.md +53 -0
- package/client/client.js +1128 -0
- package/cordis.patch.yml +6 -0
- package/dist/adapters/dmdb/index.js +326 -0
- package/dist/adapters/gaussdb/index.js +32 -0
- package/dist/adapters/index.js +41 -0
- package/dist/adapters/mongodb/index.js +508 -0
- package/dist/adapters/mysql/index.js +222 -0
- package/dist/adapters/oracle/index.js +369 -0
- package/dist/adapters/postgresql/index.js +15 -0
- package/dist/adapters/redis/index.js +492 -0
- package/dist/adapters/sql-shared/common.js +69 -0
- package/dist/adapters/sql-shared/pg-like.js +170 -0
- package/dist/adapters/sqlite/index.js +106 -0
- package/dist/adapters/types.js +1 -0
- package/dist/guard/index.js +221 -0
- package/dist/http/index.js +396 -0
- package/dist/index.js +130 -0
- package/dist/manager.js +469 -0
- package/dist/script/runner.js +147 -0
- package/dist/script/worker.cjs +104 -0
- package/dist/store/audit.js +41 -0
- package/dist/store/connections.js +167 -0
- package/dist/store/grants.js +82 -0
- package/dist/store/index.js +41 -0
- package/dist/store/io.js +66 -0
- package/dist/store/normalize.js +22 -0
- package/dist/store/secrets.js +42 -0
- package/docs/api-contract.md +76 -0
- package/docs/apple-redesign-spec.md +80 -0
- package/docs/dsh-market-submission/mengqi1436__dsh-db-tool.yml +6 -0
- package/docs/install.md +75 -0
- package/docs/review-findings.md +95 -0
- package/docs/skill.md +48 -0
- package/package.json +70 -0
- package/scripts/build-gaussdb.ps1 +72 -0
- package/scripts/build-gaussdb.sh +53 -0
- package/scripts/diag-profile.mjs +21 -0
- package/scripts/verify-host.mjs +53 -0
- package/skills/db-admin/SKILL.md +138 -0
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# dsh-db-tool bundle patch(dsh.bundle.patch:顶层数组 loader patch entries)
|
|
2
|
+
# 本地开发安装:dsh plugin --profile web add "link:E:\Code\dsh\dsh-db-tool"
|
|
3
|
+
# 发布安装(用户侧):dsh plugin --profile web add "dsh-db-tool@github:<user>/dsh-db-tool"
|
|
4
|
+
- insert:
|
|
5
|
+
- id: db-tool
|
|
6
|
+
name: dsh-db-tool
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 达梦 DM 适配器(dmdb 驱动,API 对齐 oracledb)。
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ 推断标注(未真机验证项,由 env 门控真机集成测试覆盖):
|
|
5
|
+
* - 数据字典复用 Oracle 兼容视图 ALL_TABLES / ALL_TAB_COLUMNS / ALL_CONSTRAINTS /
|
|
6
|
+
* ALL_CONS_COLUMNS(官方移植文档佐证的兼容行为)。
|
|
7
|
+
* - 分页统一 `OFFSET n ROWS FETCH FIRST m ROWS ONLY`(ANSI 风格,不依赖 compatibleMode=oracle)。
|
|
8
|
+
* - DDL 隐式提交语义按 Oracle 同等处理。
|
|
9
|
+
* - DM 事务隐式开始,无需 SET TRANSACTION。
|
|
10
|
+
*
|
|
11
|
+
* 驱动要点:
|
|
12
|
+
* - db.createPool({ connectString: 'dm://user:pass@host:5236', poolMin: 1, poolMax: 10,
|
|
13
|
+
* poolTimeout: 60, queueTimeout: 60000 });不启用压缩(默认 compress=0,规避
|
|
14
|
+
* @napi-rs/snappy 跨平台坑)。
|
|
15
|
+
* - execute 必须显式 maxRows(默认 0=无限制)与 autoCommit: true(驱动默认非自动提交)。
|
|
16
|
+
* - 返回 { metaData, rows, rowsAffected } 与 oracledb 兼容。
|
|
17
|
+
*/
|
|
18
|
+
import db from 'dmdb';
|
|
19
|
+
const ROWS_MAX = 500;
|
|
20
|
+
const CELL_TRUNC = 1000;
|
|
21
|
+
const DDL_KEYWORDS = new Set([
|
|
22
|
+
'CREATE', 'ALTER', 'DROP', 'TRUNCATE', 'RENAME', 'GRANT', 'REVOKE',
|
|
23
|
+
'COMMENT', 'ANALYZE', 'AUDIT', 'NOAUDIT', 'FLASHBACK', 'PURGE',
|
|
24
|
+
]);
|
|
25
|
+
/** 标识符基本校验(DM 官方允许 $/# 开头等宽字符集,引用创建的名字可含任意字符):
|
|
26
|
+
* 非空、≤128、无 NUL/换行。防注入靠数据字典参数绑定 + preview 处 escIdent 转义。 */
|
|
27
|
+
export function sanitizeIdentifier(name, label) {
|
|
28
|
+
if (typeof name !== 'string' || name.length === 0 || name.length > 128 ||
|
|
29
|
+
/[\0\r\n]/.test(name)) {
|
|
30
|
+
throw new Error(`非法 DM ${label}「${name}」:不允许为空、超 128 字符或含 NUL/换行`);
|
|
31
|
+
}
|
|
32
|
+
return name;
|
|
33
|
+
}
|
|
34
|
+
/** 双引号标识符转义(" → ""),拼接进 SQL 前必须过此函数 */
|
|
35
|
+
function escIdent(name) {
|
|
36
|
+
return name.replaceAll('"', '""');
|
|
37
|
+
}
|
|
38
|
+
function trunc(s, max = CELL_TRUNC) {
|
|
39
|
+
return s.length > max ? `${s.slice(0, max)}…[截断,共${s.length}字符]` : s;
|
|
40
|
+
}
|
|
41
|
+
/** 行值规范化为 NormalizedCell */
|
|
42
|
+
async function normalizeCell(v) {
|
|
43
|
+
if (v === null || v === undefined)
|
|
44
|
+
return null;
|
|
45
|
+
if (typeof v === 'number')
|
|
46
|
+
return Number.isFinite(v) ? v : String(v);
|
|
47
|
+
if (typeof v === 'string')
|
|
48
|
+
return trunc(v);
|
|
49
|
+
if (v instanceof Date)
|
|
50
|
+
return v.toISOString();
|
|
51
|
+
if (typeof v === 'bigint')
|
|
52
|
+
return v.toString();
|
|
53
|
+
if (Buffer.isBuffer(v))
|
|
54
|
+
return trunc(`0x${v.toString('hex')}`);
|
|
55
|
+
return trunc(JSON.stringify(v));
|
|
56
|
+
}
|
|
57
|
+
export function humanizeDmError(e) {
|
|
58
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
59
|
+
return new Error(`达梦 DM 错误:${msg}(提示:请检查连接串/凭据;错误码含义见达梦官方《DM8 错误码手册》)`);
|
|
60
|
+
}
|
|
61
|
+
/** 连接参数解析:url(dm://user:pass@host:5236 直接可用)或 fields 拼 connectString */
|
|
62
|
+
export function resolveDmConn(conn) {
|
|
63
|
+
if (conn.url) {
|
|
64
|
+
if (!conn.url.startsWith('dm://')) {
|
|
65
|
+
throw new Error('DM URL 应以 dm:// 开头,如 dm://user:pass@host:5236');
|
|
66
|
+
}
|
|
67
|
+
const u = new URL(conn.url);
|
|
68
|
+
return {
|
|
69
|
+
connectString: conn.url,
|
|
70
|
+
user: decodeURIComponent(u.username),
|
|
71
|
+
password: decodeURIComponent(u.password),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const f = conn.fields ?? {};
|
|
75
|
+
const host = typeof f.host === 'string' ? f.host : '';
|
|
76
|
+
const port = typeof f.port === 'number' ? f.port : 5236;
|
|
77
|
+
if (host === '') {
|
|
78
|
+
throw new Error('DM 连接需要 url(dm://user:pass@host:5236)或 fields(host/port)');
|
|
79
|
+
}
|
|
80
|
+
const user = typeof f.user === 'string' ? f.user : '';
|
|
81
|
+
const password = typeof f.password === 'string' ? f.password : '';
|
|
82
|
+
return { connectString: `dm://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}`, user, password };
|
|
83
|
+
}
|
|
84
|
+
export async function createDmAdapter(conn, opts) {
|
|
85
|
+
const mode = opts?.mode ?? 'rw';
|
|
86
|
+
let pool;
|
|
87
|
+
if (opts?.pool) {
|
|
88
|
+
pool = opts.pool;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const { connectString } = resolveDmConn(conn);
|
|
92
|
+
try {
|
|
93
|
+
pool = (await db.createPool({
|
|
94
|
+
connectString,
|
|
95
|
+
poolMin: 1,
|
|
96
|
+
poolMax: 10,
|
|
97
|
+
poolTimeout: 60,
|
|
98
|
+
queueTimeout: 60000,
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
throw humanizeDmError(e);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** 默认 execute 选项:显式 maxRows + 显式 autoCommit:true(dmdb 默认分别为 0/false,必须覆盖;SELECT 立即结束隐式事务,避免 ro 事务悬挂) */
|
|
106
|
+
const execOpts = { outFormat: db.OUT_FORMAT_OBJECT, maxRows: ROWS_MAX + 1, autoCommit: true };
|
|
107
|
+
/** 事务内 execute:显式关闭自动提交,由 tx 的 commit/rollback 收口 */
|
|
108
|
+
const execOptsTx = { ...execOpts, autoCommit: false };
|
|
109
|
+
/**
|
|
110
|
+
* 从池借一条连接执行后归还。⚠️ dmdb 的 Pool 没有 execute(与 oracledb 一致),
|
|
111
|
+
* 一切语句必须在 Connection 上执行;close() 即归还连接。
|
|
112
|
+
*/
|
|
113
|
+
async function withConn(fn) {
|
|
114
|
+
const c = await pool.getConnection();
|
|
115
|
+
try {
|
|
116
|
+
return await fn(c);
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await c.close().catch(() => { });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function requireRw(action) {
|
|
123
|
+
if (mode === 'ro') {
|
|
124
|
+
throw new Error(`连接为只读(ro)模式,拒绝执行${action};请使用 rw 连接或只发查询(query)`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function isDdl(sql) {
|
|
128
|
+
const first = sql.trim().split(/\s+/)[0]?.toUpperCase() ?? '';
|
|
129
|
+
return DDL_KEYWORDS.has(first);
|
|
130
|
+
}
|
|
131
|
+
async function toExecResult(sql, r) {
|
|
132
|
+
// ⚠️ 推断:DDL 隐式提交语义按 Oracle 同等处理(未真机验证)
|
|
133
|
+
const ddlNote = isDdl(sql) ? '(DDL 已隐式提交,不可回滚)' : '';
|
|
134
|
+
return {
|
|
135
|
+
affectedRows: typeof r.rowsAffected === 'number' && r.rowsAffected > 0 ? r.rowsAffected : undefined,
|
|
136
|
+
message: `${sql.trim().split(/\s+/)[0]?.toUpperCase() ?? 'SQL'} 完成${r.rowsAffected ? `,影响 ${r.rowsAffected} 行` : ''}${ddlNote}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async function rowsToQueryResult(r) {
|
|
140
|
+
const allRows = r.rows ?? [];
|
|
141
|
+
const truncated = allRows.length > ROWS_MAX;
|
|
142
|
+
const columns = (r.metaData ?? []).map((m) => m.name);
|
|
143
|
+
const rows = [];
|
|
144
|
+
for (const row of allRows.slice(0, ROWS_MAX)) {
|
|
145
|
+
rows.push(await Promise.all(Object.values(row).map(normalizeCell)));
|
|
146
|
+
}
|
|
147
|
+
return { columns, rows, rowCount: rows.length, truncated: truncated || undefined };
|
|
148
|
+
}
|
|
149
|
+
/** owner 解析:database 参数或默认当前用户(保留原样——引用创建的 schema 大小写敏感) */
|
|
150
|
+
function ownerOf(database) {
|
|
151
|
+
return sanitizeIdentifier(database ?? connUser, 'schema/owner 名');
|
|
152
|
+
}
|
|
153
|
+
/** DM 内建系统 schema:受安全体系保护,任何用户(含 SYSDBA)无 SELECT 权,列表中过滤 */
|
|
154
|
+
const DM_SYSTEM_SCHEMAS = new Set(['SYS', 'SYSSSO', 'SYSAUDITOR', 'SYSSCO', 'CTISYS']);
|
|
155
|
+
const connUser = resolveDmConn(conn).user.toUpperCase();
|
|
156
|
+
const adapter = {
|
|
157
|
+
kind: 'dmdb',
|
|
158
|
+
connId: conn.meta.id,
|
|
159
|
+
async testConnect() {
|
|
160
|
+
try {
|
|
161
|
+
const r = await withConn((c) => c.execute('SELECT BANNER FROM V$VERSION WHERE ROWNUM = 1', [], execOpts));
|
|
162
|
+
const row = r.rows?.[0];
|
|
163
|
+
const banner = row ? String(Object.values(row)[0] ?? '') : '';
|
|
164
|
+
return { ok: true, serverInfo: banner };
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// V$VERSION 为推断兼容项(部分 DM 版本/驱动可能不可读):
|
|
168
|
+
// 版本信息尽力而为,连通性本身用 dual 兜底判定,避免误报连接失败
|
|
169
|
+
try {
|
|
170
|
+
await withConn((c) => c.execute('SELECT 1 FROM DUAL', [], execOpts));
|
|
171
|
+
return { ok: true, serverInfo: 'DM 数据库(版本信息不可读:V$VERSION 不可用)' };
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
return { ok: false, error: humanizeDmError(e).message };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
async query(sql, params) {
|
|
179
|
+
const head = sql.trim().split(/\s+/)[0]?.toUpperCase() ?? '';
|
|
180
|
+
if (head !== 'SELECT' && head !== 'WITH') {
|
|
181
|
+
throw new Error('DM query 仅允许 SELECT/WITH 语句;写操作或 DDL 请走 execute');
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const r = await withConn((c) => c.execute(sql, params ?? [], execOpts));
|
|
185
|
+
return await rowsToQueryResult(r);
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
throw humanizeDmError(e);
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
async execute(statement, params) {
|
|
192
|
+
requireRw('execute');
|
|
193
|
+
try {
|
|
194
|
+
// 单语句语义:autoCommit: true(execOpts 默认已开启)
|
|
195
|
+
const r = await withConn((c) => c.execute(statement, params ?? [], execOpts));
|
|
196
|
+
return await toExecResult(statement, r);
|
|
197
|
+
}
|
|
198
|
+
catch (e) {
|
|
199
|
+
throw humanizeDmError(e);
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
/** 事务:专用连接多条语句,成功 commit、异常 rollback。DM 事务隐式开始,无需 SET TRANSACTION。 */
|
|
203
|
+
async tx(fn) {
|
|
204
|
+
requireRw('事务(tx)');
|
|
205
|
+
const c = await pool.getConnection();
|
|
206
|
+
try {
|
|
207
|
+
const exec = async (sql, binds) => {
|
|
208
|
+
const r = await c.execute(sql, binds ?? [], execOptsTx);
|
|
209
|
+
return toExecResult(sql, r);
|
|
210
|
+
};
|
|
211
|
+
const out = await fn(exec);
|
|
212
|
+
await c.commit();
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
try {
|
|
217
|
+
await c.rollback();
|
|
218
|
+
}
|
|
219
|
+
catch { /* 已断开等场景忽略 */ }
|
|
220
|
+
throw humanizeDmError(e);
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
try {
|
|
224
|
+
await c.close();
|
|
225
|
+
}
|
|
226
|
+
catch { /* 归还失败忽略 */ }
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
async listDatabases() {
|
|
230
|
+
// DM 为单库多 schema 模型:「数据库」下拉列出可浏览的 schema(有表者),
|
|
231
|
+
// 失败回退当前用户 schema,绝不用 host 等占位值(会污染 owner 参数)。
|
|
232
|
+
// 系统安全 schema(SYSSSO=MAC 标记/SYSAUDITOR=审计/SYSSCO/CTISYS=全密/SYS)
|
|
233
|
+
// 连 SYSDBA 也无 SELECT 权(DM 有意保护),列出只会让展开时报 [-5504],
|
|
234
|
+
// 故过滤(保留当前用户自身,支持直接以系统用户连接的场景)。
|
|
235
|
+
try {
|
|
236
|
+
const r = await withConn((c) => c.execute('SELECT DISTINCT OWNER FROM ALL_TABLES ORDER BY OWNER', [], execOpts));
|
|
237
|
+
const owners = (r.rows ?? [])
|
|
238
|
+
.map((row) => String(Object.values(row)[0] ?? '').trim())
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.filter((o) => o === connUser || !DM_SYSTEM_SCHEMAS.has(o.toUpperCase()));
|
|
241
|
+
return owners.length > 0 ? owners : [connUser];
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return [connUser];
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
async listTables(database) {
|
|
248
|
+
const owner = ownerOf(database);
|
|
249
|
+
try {
|
|
250
|
+
// ⚠️ 推断:DM 兼容 Oracle 数据字典 ALL_TABLES(未真机验证)
|
|
251
|
+
const r = await withConn((c) => c.execute('SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER = :owner ORDER BY TABLE_NAME OFFSET 0 ROWS FETCH FIRST 500 ROWS ONLY', [owner], execOpts));
|
|
252
|
+
const rows = r.rows ?? [];
|
|
253
|
+
return rows.map((row) => ({ name: String(row.TABLE_NAME ?? ''), type: 'TABLE' }));
|
|
254
|
+
}
|
|
255
|
+
catch (e) {
|
|
256
|
+
throw humanizeDmError(e);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
async describeTable(table, database) {
|
|
260
|
+
const owner = ownerOf(database);
|
|
261
|
+
// 原名精确匹配优先;未引用创建的表名在字典中为大写存储,UPPER 兜底
|
|
262
|
+
const tab = sanitizeIdentifier(table, '表名');
|
|
263
|
+
try {
|
|
264
|
+
// ⚠️ 推断:ALL_TAB_COLUMNS / ALL_CONSTRAINTS / ALL_CONS_COLUMNS 兼容(未真机验证);同连接内两查保证一致性
|
|
265
|
+
const { colsR, pkR } = await withConn(async (c) => ({
|
|
266
|
+
colsR: await c.execute(`SELECT COLUMN_NAME, DATA_TYPE, DATA_LENGTH, DATA_PRECISION, DATA_SCALE, NULLABLE, DATA_DEFAULT
|
|
267
|
+
FROM ALL_TAB_COLUMNS WHERE OWNER = :owner AND (TABLE_NAME = :tab OR TABLE_NAME = UPPER(:tab)) ORDER BY COLUMN_ID`, [owner, tab], execOpts),
|
|
268
|
+
pkR: await c.execute(`SELECT COLS.COLUMN_NAME
|
|
269
|
+
FROM ALL_CONSTRAINTS CONS
|
|
270
|
+
JOIN ALL_CONS_COLUMNS COLS
|
|
271
|
+
ON CONS.OWNER = COLS.OWNER AND CONS.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME
|
|
272
|
+
WHERE CONS.CONSTRAINT_TYPE = 'P' AND CONS.OWNER = :owner AND (CONS.TABLE_NAME = :tab OR CONS.TABLE_NAME = UPPER(:tab))`, [owner, tab], execOpts),
|
|
273
|
+
}));
|
|
274
|
+
const pkSet = new Set((pkR.rows ?? []).map((r2) => String(r2.COLUMN_NAME ?? '')));
|
|
275
|
+
return (colsR.rows ?? []).map((row) => {
|
|
276
|
+
const dataType = String(row.DATA_TYPE ?? '');
|
|
277
|
+
const precision = row.DATA_PRECISION;
|
|
278
|
+
const scale = row.DATA_SCALE;
|
|
279
|
+
const length = row.DATA_LENGTH;
|
|
280
|
+
let typeStr = dataType;
|
|
281
|
+
if (precision !== null && precision !== undefined) {
|
|
282
|
+
typeStr = scale !== null && scale !== undefined && Number(scale) > 0
|
|
283
|
+
? `${dataType}(${Number(precision)},${Number(scale)})`
|
|
284
|
+
: `${dataType}(${Number(precision)})`;
|
|
285
|
+
}
|
|
286
|
+
else if (/^(VARCHAR|NVARCHAR|CHAR|NCHAR|RAW|BIT)/.test(dataType) && length !== null && length !== undefined) {
|
|
287
|
+
typeStr = `${dataType}(${Number(length)})`;
|
|
288
|
+
}
|
|
289
|
+
const def = row.DATA_DEFAULT;
|
|
290
|
+
return {
|
|
291
|
+
name: String(row.COLUMN_NAME ?? ''),
|
|
292
|
+
dataType: typeStr,
|
|
293
|
+
nullable: String(row.NULLABLE ?? 'Y') === 'Y',
|
|
294
|
+
...(pkSet.has(String(row.COLUMN_NAME ?? '')) ? { key: 'PRI' } : {}),
|
|
295
|
+
default: def === null || def === undefined ? null : trunc(String(def).trim()),
|
|
296
|
+
};
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
catch (e) {
|
|
300
|
+
throw humanizeDmError(e);
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
async previewRows(table, limit, database, offset) {
|
|
304
|
+
const owner = ownerOf(database);
|
|
305
|
+
const tab = sanitizeIdentifier(table, '表名'); // 拼接前经 escIdent 转义
|
|
306
|
+
const n = Math.max(1, Math.min(Math.floor(limit) || 20, ROWS_MAX));
|
|
307
|
+
const off = Math.max(0, Math.floor(offset ?? 0) || 0);
|
|
308
|
+
try {
|
|
309
|
+
// 统一 ANSI 分页(不依赖 compatibleMode=oracle),OFFSET/FETCH 均走 bind ⚠️ 推断(未真机验证)
|
|
310
|
+
const r = await withConn((c) => c.execute(`SELECT * FROM "${escIdent(owner)}"."${escIdent(tab)}" OFFSET :o ROWS FETCH FIRST :n ROWS ONLY`, [off, n], execOpts));
|
|
311
|
+
return await rowsToQueryResult(r);
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
throw humanizeDmError(e);
|
|
315
|
+
}
|
|
316
|
+
},
|
|
317
|
+
async close() {
|
|
318
|
+
try {
|
|
319
|
+
await pool.close();
|
|
320
|
+
}
|
|
321
|
+
catch { /* 已关闭忽略 */ }
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
return adapter;
|
|
325
|
+
}
|
|
326
|
+
export const factory = async (conn) => createDmAdapter(conn);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GaussDB(openGauss)适配器。
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ 官方驱动 openGauss-connector-nodejs 未发布 npm,按 openGauss 官方 README/文档实现,
|
|
5
|
+
* 使用 vendor/gaussdb-pg 构建产物(fork 自 pg 8.7 时代,API 与 pg 完全兼容),未真机验证。
|
|
6
|
+
*
|
|
7
|
+
* 构建方式:bash scripts/build-gaussdb.sh(或 npm run build:gaussdb)
|
|
8
|
+
* 核心逻辑与 postgresql 适配器共享(sql-shared/pg-like.ts)。
|
|
9
|
+
*/
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
11
|
+
import { createPgLikeAdapter, } from '../sql-shared/pg-like.js';
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
// packages 布局:pg lib 内 require('../../pg-protocol'/'../../pg-pool') 按 monorepo 假设向上两级解析
|
|
14
|
+
// 相对 createRequire base(lib/adapters/gaussdb/)需三级到项目根:gaussdb/ → adapters/ → lib/ → root
|
|
15
|
+
const VENDOR_PATH = '../../../vendor/gaussdb-pg/packages/pg';
|
|
16
|
+
function loadGaussDriver() {
|
|
17
|
+
try {
|
|
18
|
+
// vendor 为 CJS 产物,结构同 pg:module.exports = { Pool, Client, ... }
|
|
19
|
+
return require(VENDOR_PATH);
|
|
20
|
+
}
|
|
21
|
+
catch (e) {
|
|
22
|
+
// 附原始错误:区分「vendor 未构建」与「构建产物损坏/依赖缺失」两类故障
|
|
23
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
24
|
+
throw new Error('GaussDB 驱动未找到:官方 openGauss-connector-nodejs 未发布 npm,' +
|
|
25
|
+
'请先执行 bash scripts/build-gaussdb.sh(或 npm run build:gaussdb)构建 vendor/gaussdb-pg。' +
|
|
26
|
+
`原始错误: ${msg}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function createGaussdbAdapter(conn, opts) {
|
|
30
|
+
return createPgLikeAdapter('gaussdb', loadGaussDriver(), conn, opts);
|
|
31
|
+
}
|
|
32
|
+
export const factory = (conn) => createGaussdbAdapter(conn);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const LOADERS = {
|
|
2
|
+
mysql: () => import('./mysql/index.js').then((m) => m.createMysqlAdapter),
|
|
3
|
+
postgresql: () => import('./postgresql/index.js').then((m) => m.createPostgresqlAdapter),
|
|
4
|
+
gaussdb: () => import('./gaussdb/index.js').then((m) => m.createGaussdbAdapter),
|
|
5
|
+
sqlite: () => import('./sqlite/index.js').then((m) => m.createSqliteAdapter),
|
|
6
|
+
redis: () => import('./redis/index.js').then((m) => m.createRedisAdapter),
|
|
7
|
+
mongodb: () => import('./mongodb/index.js').then((m) => m.createMongoAdapter),
|
|
8
|
+
oracle: () => import('./oracle/index.js').then((m) => m.createOracleAdapter),
|
|
9
|
+
dmdb: () => import('./dmdb/index.js').then((m) => m.createDmAdapter),
|
|
10
|
+
};
|
|
11
|
+
/** 驱动缺失时的安装指引 */
|
|
12
|
+
const INSTALL_HINTS = {
|
|
13
|
+
mysql: 'npm install mysql2',
|
|
14
|
+
postgresql: 'npm install pg',
|
|
15
|
+
gaussdb: 'npm run build:gaussdb(构建 vendor/gaussdb-pg)',
|
|
16
|
+
sqlite: 'npm install better-sqlite3',
|
|
17
|
+
redis: 'npm install redis',
|
|
18
|
+
mongodb: 'npm install mongodb',
|
|
19
|
+
oracle: 'npm install oracledb',
|
|
20
|
+
dmdb: 'npm install dmdb',
|
|
21
|
+
};
|
|
22
|
+
/** 生成带安装指引的驱动缺失错误 */
|
|
23
|
+
export function driverMissingError(kind, cause) {
|
|
24
|
+
const detail = cause instanceof Error ? cause.message : String(cause ?? '');
|
|
25
|
+
const err = new Error(`${kind} 驱动不可用${detail ? `:${detail}` : ''}。请安装/构建后重试:${INSTALL_HINTS[kind]}`);
|
|
26
|
+
err.name = 'DriverMissingError';
|
|
27
|
+
return err;
|
|
28
|
+
}
|
|
29
|
+
const cache = new Map();
|
|
30
|
+
/** 取某 kind 的适配器工厂(惰性加载并缓存;失败清缓存以便重试) */
|
|
31
|
+
export function getAdapter(kind) {
|
|
32
|
+
let p = cache.get(kind);
|
|
33
|
+
if (!p) {
|
|
34
|
+
p = LOADERS[kind]().catch((e) => {
|
|
35
|
+
cache.delete(kind);
|
|
36
|
+
throw driverMissingError(kind, e);
|
|
37
|
+
});
|
|
38
|
+
cache.set(kind, p);
|
|
39
|
+
}
|
|
40
|
+
return p;
|
|
41
|
+
}
|