dsh-db-tool 1.5.91 → 1.7.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/client/client.js +980 -31
- package/dist/adapters/dmdb/index.js +25 -4
- package/dist/adapters/gaussdb/index.js +2 -0
- package/dist/adapters/mongodb/index.js +47 -8
- package/dist/adapters/mysql/index.js +31 -5
- package/dist/adapters/oracle/index.js +39 -9
- package/dist/adapters/postgresql/index.js +4 -0
- package/dist/adapters/sql-shared/pg-like.js +54 -16
- package/dist/adapters/sqlite/index.js +50 -19
- package/dist/adapters/transfer/kinds/mongo.js +378 -0
- package/dist/adapters/transfer/kinds/mysql.js +331 -0
- package/dist/adapters/transfer/kinds/oralike.js +437 -0
- package/dist/adapters/transfer/kinds/pg.js +311 -0
- package/dist/adapters/transfer/kinds/sqlite.js +250 -0
- package/dist/adapters/transfer/model.js +77 -0
- package/dist/http/index.js +74 -0
- package/dist/manager.js +239 -2
- package/dist/store/connections.js +85 -2
- package/dist/store/export-crypto.js +78 -0
- package/dist/store/index.js +7 -1
- package/dist/store/transfer-history.js +114 -0
- package/dist/transfer.js +287 -0
- package/package.json +1 -1
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { sanitizeIdentifier as oraSanitize } from '../../oracle/index.js';
|
|
2
|
+
import { safeDefaultExpr } from '../model.js';
|
|
3
|
+
/** DATA_TYPE → 中间类型(NUMBER 按 scale/precision 分档防窄列;TSLTZ 归 timestamptz) */
|
|
4
|
+
export function oraTypeToMid(dataType, precision, scale) {
|
|
5
|
+
const t = dataType.toUpperCase();
|
|
6
|
+
if (t === 'NUMBER' || t === 'DECIMAL' || t === 'NUMERIC' || t === 'DEC') {
|
|
7
|
+
const s = scale ?? 0;
|
|
8
|
+
if (s === 0 && precision != null) {
|
|
9
|
+
if (precision <= 9)
|
|
10
|
+
return 'int';
|
|
11
|
+
if (precision <= 18)
|
|
12
|
+
return 'bigint';
|
|
13
|
+
}
|
|
14
|
+
return 'decimal';
|
|
15
|
+
}
|
|
16
|
+
if (t === 'FLOAT' || t === 'BINARY_FLOAT')
|
|
17
|
+
return 'float';
|
|
18
|
+
if (t === 'BINARY_DOUBLE' || t === 'DOUBLE' || t === 'REAL')
|
|
19
|
+
return 'double';
|
|
20
|
+
if (t === 'BIT' || t === 'BOOLEAN')
|
|
21
|
+
return 'boolean';
|
|
22
|
+
if (t === 'CLOB' || t === 'NCLOB' || t === 'LONG' || t === 'TEXT')
|
|
23
|
+
return 'text';
|
|
24
|
+
if (t === 'BLOB' || t === 'RAW' || t === 'LONG RAW' || t === 'BFILE' || t === 'BINARY' || t === 'VARBINARY' || t === 'IMAGE')
|
|
25
|
+
return 'bytes';
|
|
26
|
+
if (t === 'DATE')
|
|
27
|
+
return 'datetime'; // Oracle DATE 含时间部分
|
|
28
|
+
if (t.startsWith('TIMESTAMP'))
|
|
29
|
+
return t.includes('TIME ZONE') ? 'timestamptz' : 'datetime';
|
|
30
|
+
if (t === 'TIME')
|
|
31
|
+
return 'time';
|
|
32
|
+
return 'string'; // VARCHAR2/NVARCHAR2/CHAR/NCHAR/INTERVAL/其他
|
|
33
|
+
}
|
|
34
|
+
/** 中间类型 → Oracle 系目标列类型(DM 与 Oracle 同族;oracle 无原生 TIME/BOOLEAN 的降级在此) */
|
|
35
|
+
export function midToOraType(c, isDm) {
|
|
36
|
+
switch (c.midType) {
|
|
37
|
+
case 'int': return isDm ? 'INT' : 'NUMBER(10)';
|
|
38
|
+
case 'bigint': return isDm ? 'BIGINT' : 'NUMBER(19)';
|
|
39
|
+
case 'decimal': return isDm ? `DECIMAL(${c.precision ?? 38},${c.scale ?? 10})` : `NUMBER(${c.precision ?? 38},${c.scale ?? 10})`;
|
|
40
|
+
case 'float': return isDm ? 'FLOAT' : 'BINARY_FLOAT';
|
|
41
|
+
case 'double': return isDm ? 'DOUBLE' : 'BINARY_DOUBLE';
|
|
42
|
+
case 'boolean': return isDm ? 'BIT' : 'NUMBER(1)';
|
|
43
|
+
case 'string': return `VARCHAR2(${Math.min(Math.max(c.length ?? 255, 1), 4000)})`;
|
|
44
|
+
case 'text': return isDm ? 'CLOB' : 'CLOB';
|
|
45
|
+
case 'bytes': return 'BLOB';
|
|
46
|
+
case 'json': return 'CLOB';
|
|
47
|
+
case 'date': return 'DATE';
|
|
48
|
+
case 'time': return isDm ? 'TIME' : 'VARCHAR2(64)'; // oracle 无原生 TIME:墙钟字符串保真
|
|
49
|
+
case 'datetime': return 'TIMESTAMP';
|
|
50
|
+
case 'timestamptz': return 'TIMESTAMP WITH TIME ZONE';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** 绑定值形态:bigint→字符串(整数串无 NLS 分隔符风险)、boolean→1/0(NUMBER(1)/BIT)、
|
|
54
|
+
* 日期列统一 'YYYY-MM-DD HH:MM:SS' UTC 墙钟(col 缺省时不做日期归一) */
|
|
55
|
+
export function toOraBind(c, col) {
|
|
56
|
+
if (c === null)
|
|
57
|
+
return null;
|
|
58
|
+
if (typeof c === 'bigint')
|
|
59
|
+
return c.toString();
|
|
60
|
+
if (typeof c === 'boolean')
|
|
61
|
+
return c ? 1 : 0;
|
|
62
|
+
if (typeof c === 'string' && col != null &&
|
|
63
|
+
(col.midType === 'date' || col.midType === 'datetime' || col.midType === 'timestamptz')) {
|
|
64
|
+
return toOraDateTime(c);
|
|
65
|
+
}
|
|
66
|
+
return c;
|
|
67
|
+
}
|
|
68
|
+
/** 中间格式日期值(ISO 带 'Z' / 裸墙钟两种形态)→ 单一 UTC 墙钟串。
|
|
69
|
+
* Oracle/DM 隐式字符串→日期转换按会话 NLS_DATE_FORMAT(默认 'DD-MON-RR' 系)
|
|
70
|
+
* 解析,ISO 'T'/'Z' 必抛 ORA-01861/01858——写端会话格式已对齐本形态(ensureSession) */
|
|
71
|
+
export function toOraDateTime(s) {
|
|
72
|
+
return s.replace(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}:\d{2}:\d{2})(?:\.\d+)?Z$/i, '$1 $2');
|
|
73
|
+
}
|
|
74
|
+
function isOraDateMid(t) {
|
|
75
|
+
return t === 'date' || t === 'datetime' || t === 'timestamptz';
|
|
76
|
+
}
|
|
77
|
+
/** 会话前置语句(读端 ensureConn / 写端 ensureSession 共用),按 kind 分流:
|
|
78
|
+
* - 时区:Oracle 官方语法为 ALTER SESSION SET TIME_ZONE(Oracle SQL 手册);
|
|
79
|
+
* DM 官方语法为 SET TIME ZONE '<值>'(《DM8 SQL 语言使用手册》3.14.1),
|
|
80
|
+
* Oracle 风格「ALTER SESSION SET TIME_ZONE」在 DM 手册查无出处,不得混用。
|
|
81
|
+
* - NLS_NUMERIC_CHARACTERS:Oracle 官方支持经 ALTER SESSION 设置;《DM8 SQL 语言
|
|
82
|
+
* 使用手册》3.14 可经 ALTER SESSION 设置的 NLS 参数清单(NLS_DATE_LANGUAGE/
|
|
83
|
+
* NLS_DATE_FORMAT/NLS_TIMESTAMP_FORMAT/NLS_TIMESTAMP_TZ_FORMAT/NLS_SORT/
|
|
84
|
+
* CASE_SENSITIVE)不含该参数 → DM 不下发(发了被拒即静默失效;decimal 绑定
|
|
85
|
+
* 的 NLS 保护仅 Oracle 会话适用)。
|
|
86
|
+
* - NLS 日期/时间戳格式:两库官方清单均含,统一 'YYYY-MM-DD HH24:MI:SS',
|
|
87
|
+
* 与 toOraDateTime 归一形态对齐(keyset/range 分片键以字符串绑定做 k > lo
|
|
88
|
+
* 比较,否则 ISO 串按默认 DD-MON-RR 隐式转换整批 ORA-01861)。
|
|
89
|
+
* 调用方对单条失败吞错(兼容差异不影响主路径)。 */
|
|
90
|
+
export function oralikeSessionStmts(kind) {
|
|
91
|
+
const nlsFormats = [
|
|
92
|
+
"ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'",
|
|
93
|
+
"ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS'",
|
|
94
|
+
"ALTER SESSION SET NLS_TIMESTAMP_TZ_FORMAT = 'YYYY-MM-DD HH24:MI:SS'",
|
|
95
|
+
];
|
|
96
|
+
return kind === 'dmdb'
|
|
97
|
+
? ["SET TIME ZONE '+00:00'", ...nlsFormats]
|
|
98
|
+
: ["ALTER SESSION SET TIME_ZONE = '+00:00'", "ALTER SESSION SET NLS_NUMERIC_CHARACTERS = '. '", ...nlsFormats];
|
|
99
|
+
}
|
|
100
|
+
/** 写端会话前置:日期统一按 'YYYY-MM-DD HH24:MI:SS'(+会话 UTC)隐式转换 */
|
|
101
|
+
async function ensureSession(conn, kind) {
|
|
102
|
+
for (const stmt of oralikeSessionStmts(kind)) {
|
|
103
|
+
await conn.execute(stmt).catch(() => { });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** 行值保真:Date→ISO;Lob(CLOB 残留对象)→ getData 拉全量 */
|
|
107
|
+
async function materialize(v) {
|
|
108
|
+
if (v === null || v === undefined)
|
|
109
|
+
return null;
|
|
110
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'bigint' || typeof v === 'boolean')
|
|
111
|
+
return v;
|
|
112
|
+
if (v instanceof Date)
|
|
113
|
+
return v.toISOString();
|
|
114
|
+
if (v instanceof Uint8Array)
|
|
115
|
+
return v;
|
|
116
|
+
const lob = v;
|
|
117
|
+
if (typeof lob.getData === 'function') {
|
|
118
|
+
const data = await lob.getData();
|
|
119
|
+
// Lob 用毕防御性 close:dmdb 官方仅声明「ResultSet和Lob对象不支持并发操作」,
|
|
120
|
+
// 未说明 getData 后是否必须 close(oracledb 官方要求 Lob 用毕 close)——
|
|
121
|
+
// 驱动有 close 声明(index.d.ts Lob 接口)即调用,失败忽略
|
|
122
|
+
try {
|
|
123
|
+
await lob.close?.();
|
|
124
|
+
}
|
|
125
|
+
catch { /* 释放失败忽略 */ }
|
|
126
|
+
return data === undefined ? null : data;
|
|
127
|
+
}
|
|
128
|
+
return String(v);
|
|
129
|
+
}
|
|
130
|
+
function escIdent(name) {
|
|
131
|
+
return name.replaceAll('"', '""');
|
|
132
|
+
}
|
|
133
|
+
function sanitize(name, label) {
|
|
134
|
+
// oracle/dmdb 各自导出的 sanitizeIdentifier 语义一致(非空、≤128、无 NUL/换行)
|
|
135
|
+
return oraSanitize(name, label);
|
|
136
|
+
}
|
|
137
|
+
export function createOraLikeTransferReader(kind, pool, owner) {
|
|
138
|
+
// 专用连接:会话前置一次(时区/NLS,按 kind 分流见 oralikeSessionStmts),
|
|
139
|
+
// 所有批读同连接保证游标一致性
|
|
140
|
+
let conn = null;
|
|
141
|
+
const ensureConn = async () => {
|
|
142
|
+
if (conn)
|
|
143
|
+
return conn;
|
|
144
|
+
conn = await pool.getConnection();
|
|
145
|
+
for (const stmt of oralikeSessionStmts(kind)) {
|
|
146
|
+
await conn.execute(stmt).catch(() => { });
|
|
147
|
+
}
|
|
148
|
+
return conn;
|
|
149
|
+
};
|
|
150
|
+
const exec = async (sql, binds) => (await (await ensureConn()).execute(sql, binds, { outFormat: 4002 /* OBJECT */, maxRows: 0, autoCommit: true })).rows ?? [];
|
|
151
|
+
const describe = async (table) => {
|
|
152
|
+
const own = sanitize(owner, 'owner');
|
|
153
|
+
const tab = sanitize(table, '表名');
|
|
154
|
+
const rows = await exec(`SELECT COLUMN_NAME, DATA_TYPE, DATA_PRECISION, DATA_SCALE, CHAR_LENGTH, NULLABLE, DATA_DEFAULT
|
|
155
|
+
FROM ALL_TAB_COLUMNS WHERE OWNER = :o AND (TABLE_NAME = :t OR TABLE_NAME = UPPER(:t))
|
|
156
|
+
ORDER BY COLUMN_ID`, [own, tab]);
|
|
157
|
+
if (rows.length === 0)
|
|
158
|
+
throw new Error(`表不存在: ${own}.${tab}`);
|
|
159
|
+
const pks = await exec(`SELECT COLS.COLUMN_NAME FROM ALL_CONSTRAINTS CONS
|
|
160
|
+
JOIN ALL_CONS_COLUMNS COLS ON CONS.OWNER = COLS.OWNER AND CONS.CONSTRAINT_NAME = COLS.CONSTRAINT_NAME
|
|
161
|
+
WHERE CONS.CONSTRAINT_TYPE = 'P' AND CONS.OWNER = :o AND (CONS.TABLE_NAME = :t OR CONS.TABLE_NAME = UPPER(:t))`, [own, tab]);
|
|
162
|
+
const pkSet = new Set(pks.map((r) => String(Object.values(r)[0] ?? '')));
|
|
163
|
+
const columns = [];
|
|
164
|
+
for (const r of rows) {
|
|
165
|
+
const dataType = String(r.DATA_TYPE ?? '');
|
|
166
|
+
const precision = r.DATA_PRECISION == null ? null : Number(r.DATA_PRECISION);
|
|
167
|
+
const scale = r.DATA_SCALE == null ? null : Number(r.DATA_SCALE);
|
|
168
|
+
const def = r.DATA_DEFAULT;
|
|
169
|
+
columns.push({
|
|
170
|
+
name: String(r.COLUMN_NAME ?? ''),
|
|
171
|
+
midType: oraTypeToMid(dataType, precision, scale),
|
|
172
|
+
sourceType: dataType,
|
|
173
|
+
nullable: String(r.NULLABLE ?? 'Y') === 'Y',
|
|
174
|
+
primaryKey: pkSet.has(String(r.COLUMN_NAME ?? '')),
|
|
175
|
+
length: r.CHAR_LENGTH == null ? undefined : Number(r.CHAR_LENGTH),
|
|
176
|
+
precision: precision ?? undefined,
|
|
177
|
+
scale: scale ?? undefined,
|
|
178
|
+
// DATA_DEFAULT 是 LONG 列:驱动可能返回字符串(thin 模式)或截断——白名单兜底
|
|
179
|
+
default: def == null ? null : String(def).trim(),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return { name: tab, columns };
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
kind,
|
|
186
|
+
listTables: async () => {
|
|
187
|
+
const own = sanitize(owner, 'owner');
|
|
188
|
+
const rows = await exec(`SELECT TABLE_NAME FROM ALL_TABLES WHERE OWNER = :o ORDER BY TABLE_NAME`, [own]);
|
|
189
|
+
return rows.map((r) => String(Object.values(r)[0] ?? ''));
|
|
190
|
+
},
|
|
191
|
+
describe,
|
|
192
|
+
count: async (table) => {
|
|
193
|
+
const own = escIdent(sanitize(owner, 'owner'));
|
|
194
|
+
const tab = escIdent(sanitize(table, '表名'));
|
|
195
|
+
const rows = await exec(`SELECT COUNT(*) AS N FROM "${own}"."${tab}"`);
|
|
196
|
+
return Number(Object.values(rows[0] ?? {})[0] ?? 0);
|
|
197
|
+
},
|
|
198
|
+
shardKey: async (table) => {
|
|
199
|
+
const t = await describe(table);
|
|
200
|
+
const pks = t.columns.filter((c) => c.primaryKey);
|
|
201
|
+
const pk = pks.length === 1 ? pks[0] : undefined;
|
|
202
|
+
const sharable = ['int', 'bigint', 'decimal', 'string', 'datetime', 'timestamptz'];
|
|
203
|
+
return pk && sharable.includes(pk.midType) ? pk.midType : null;
|
|
204
|
+
},
|
|
205
|
+
readBatch: async (table, cursor, limit) => {
|
|
206
|
+
const t = await describe(table);
|
|
207
|
+
// 表/分片键与同文件 count/keyAt 同走 sanitize(防 NUL/换行/超长的统一防线)
|
|
208
|
+
const tbl = `"${escIdent(sanitize(t.name, '表名'))}"`;
|
|
209
|
+
const cols = t.columns.map((c) => `"${escIdent(c.name)}"`).join(', ');
|
|
210
|
+
// 分片键(date 系列时为 materialize 产出的 ISO 串)先归一 UTC 墙钟再绑定
|
|
211
|
+
const keyCol = cursor.mode === 'offset' ? undefined : t.columns.find((c) => c.name === cursor.column);
|
|
212
|
+
const bindKey = (v) => typeof v === 'string' && keyCol != null && isOraDateMid(keyCol.midType) ? toOraDateTime(v) : toOraBind(v);
|
|
213
|
+
let sql;
|
|
214
|
+
let binds;
|
|
215
|
+
// ROW_LIMIT 子句语法图的 <offset>/<大小> 均为字面 <整数>(DM8 SQL 手册
|
|
216
|
+
// 数据查询语句章),绑定占位符能否用于该位置无官方佐证 → 内联经 clamp 的
|
|
217
|
+
// 整数(值源自编排层内部 offset/limit,非用户拼串,无注入面)
|
|
218
|
+
const n = Math.max(1, Math.floor(limit) || 1);
|
|
219
|
+
if (cursor.mode === 'keyset') {
|
|
220
|
+
const key = `"${escIdent(sanitize(cursor.column, '分片键'))}"`;
|
|
221
|
+
if (cursor.after === null) {
|
|
222
|
+
sql = `SELECT ${cols} FROM ${tbl} ORDER BY ${key} OFFSET 0 ROWS FETCH FIRST ${n} ROWS ONLY`;
|
|
223
|
+
binds = [];
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
sql = `SELECT ${cols} FROM ${tbl} WHERE ${key} > :after ORDER BY ${key} OFFSET 0 ROWS FETCH FIRST ${n} ROWS ONLY`;
|
|
227
|
+
binds = [bindKey(cursor.after)];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (cursor.mode === 'range') {
|
|
231
|
+
const key = `"${escIdent(sanitize(cursor.column, '分片键'))}"`;
|
|
232
|
+
const conds = [];
|
|
233
|
+
const bindsArr = [];
|
|
234
|
+
if (cursor.lo !== null) {
|
|
235
|
+
conds.push(`${key} ${cursor.loInclusive ? '>=' : '>'} :lo`);
|
|
236
|
+
bindsArr.push(bindKey(cursor.lo));
|
|
237
|
+
}
|
|
238
|
+
if (cursor.hi !== null) {
|
|
239
|
+
conds.push(`${key} < :hi`);
|
|
240
|
+
bindsArr.push(bindKey(cursor.hi));
|
|
241
|
+
}
|
|
242
|
+
const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
|
|
243
|
+
sql = `SELECT ${cols} FROM ${tbl}${where} ORDER BY ${key} OFFSET 0 ROWS FETCH FIRST ${n} ROWS ONLY`;
|
|
244
|
+
binds = bindsArr;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
sql = `SELECT ${cols} FROM ${tbl} OFFSET ${Math.max(0, Math.floor(cursor.offset) || 0)} ROWS FETCH FIRST ${n} ROWS ONLY`;
|
|
248
|
+
binds = [];
|
|
249
|
+
}
|
|
250
|
+
const rows = await exec(sql, binds);
|
|
251
|
+
const out = [];
|
|
252
|
+
for (const r of rows) {
|
|
253
|
+
const row = [];
|
|
254
|
+
for (const c of t.columns) {
|
|
255
|
+
row.push(await materialize(r[c.name]));
|
|
256
|
+
}
|
|
257
|
+
out.push(row);
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
},
|
|
261
|
+
keyAt: async (table, column, offset) => {
|
|
262
|
+
const own = escIdent(sanitize(owner, 'owner'));
|
|
263
|
+
const key = `"${escIdent(sanitize(column, '分片键'))}"`;
|
|
264
|
+
const rows = await exec(`SELECT ${key} AS K FROM "${own}"."${escIdent(sanitize(table, '表名'))}" ORDER BY ${key} OFFSET ${Math.max(0, Math.floor(offset) || 0)} ROWS FETCH FIRST 1 ROWS ONLY`);
|
|
265
|
+
return rows.length === 0 ? null : await materialize(Object.values(rows[0] ?? {})[0]);
|
|
266
|
+
},
|
|
267
|
+
close: async () => {
|
|
268
|
+
if (conn) {
|
|
269
|
+
await conn.close().catch(() => { });
|
|
270
|
+
conn = null;
|
|
271
|
+
}
|
|
272
|
+
await pool.close().catch(() => { });
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
/** 写端:owner = 目标 schema(定位第一层,缺省连接用户默认 schema——调用方传
|
|
277
|
+
* ownerOfFromConn(rc)),表名以 owner 限定(reader 内 describe/listTables 同款样板) */
|
|
278
|
+
export function createOraLikeTransferWriter(kind, pool, writeMode = 'insert', owner) {
|
|
279
|
+
const ownerQ = escIdent(sanitize(owner, 'owner'));
|
|
280
|
+
const qual = (name) => `"${ownerQ}"."${escIdent(name)}"`;
|
|
281
|
+
return {
|
|
282
|
+
kind,
|
|
283
|
+
tableExists: async (name) => {
|
|
284
|
+
const conn = await pool.getConnection();
|
|
285
|
+
try {
|
|
286
|
+
const r = await conn.execute(`SELECT COUNT(*) AS N FROM ALL_TABLES WHERE OWNER = :o AND TABLE_NAME = :t`, [sanitize(owner, 'owner'), sanitize(name, '表名')], { maxRows: 1 });
|
|
287
|
+
const rows = r.rows ?? [];
|
|
288
|
+
return Number(Object.values(rows[0] ?? {})[0] ?? 0) > 0;
|
|
289
|
+
}
|
|
290
|
+
finally {
|
|
291
|
+
await conn.close().catch(() => { });
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
dropTable: async (name) => {
|
|
295
|
+
const conn = await pool.getConnection();
|
|
296
|
+
try {
|
|
297
|
+
await conn.execute(`DROP TABLE "${escIdent(sanitize(name, '表名'))}"`, [], { autoCommit: true });
|
|
298
|
+
}
|
|
299
|
+
finally {
|
|
300
|
+
await conn.close().catch(() => { });
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
// truncate 模式前置清空:TRUNCATE TABLE 重置高水位(Oracle/DM 官方手册;DDL 隐式提交,此处独占前置时机无影响)
|
|
304
|
+
beforeLoad: async (table) => {
|
|
305
|
+
const conn = await pool.getConnection();
|
|
306
|
+
try {
|
|
307
|
+
// 同连接先设 DDL 锁等待:TRUNCATE 被活跃事务持有时默认无限等(oracle ddl_lock_timeout,DM 兼容);
|
|
308
|
+
// TRUNCATE 拿不到锁/遇外键(ORA-02266)降级 DELETE(行级锁有限等待)
|
|
309
|
+
await conn.execute('ALTER SESSION SET DDL_LOCK_TIMEOUT = 15', [], { autoCommit: true }).catch(() => { });
|
|
310
|
+
try {
|
|
311
|
+
await conn.execute(`TRUNCATE TABLE ${qual(table.name)}`, [], { autoCommit: true });
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
await conn.execute(`DELETE FROM ${qual(table.name)}`, [], { autoCommit: true });
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
await conn.close().catch(() => { });
|
|
319
|
+
}
|
|
320
|
+
},
|
|
321
|
+
createTable: async (table) => {
|
|
322
|
+
const isDm = kind === 'dmdb';
|
|
323
|
+
const defs = table.columns.map((c) => {
|
|
324
|
+
let d = `"${escIdent(c.name)}" ${midToOraType(c, isDm)}`;
|
|
325
|
+
if (!c.nullable)
|
|
326
|
+
d += ' NOT NULL';
|
|
327
|
+
if (c.default != null) {
|
|
328
|
+
const expr = safeDefaultExpr(c.default);
|
|
329
|
+
if (expr != null)
|
|
330
|
+
d += ` DEFAULT ${expr}`;
|
|
331
|
+
}
|
|
332
|
+
return d;
|
|
333
|
+
});
|
|
334
|
+
const conn = await pool.getConnection();
|
|
335
|
+
try {
|
|
336
|
+
// DDL 隐式提交(oracle/dm 同族语义)
|
|
337
|
+
const pkCols = table.columns.filter((c) => c.primaryKey).map((c) => `"${escIdent(c.name)}"`);
|
|
338
|
+
const pk = pkCols.length > 0 ? `, PRIMARY KEY (${pkCols.join(', ')})` : '';
|
|
339
|
+
await conn.execute(`CREATE TABLE ${qual(table.name)} (${defs.join(', ')}${pk})`, [], { autoCommit: true });
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
await conn.close().catch(() => { });
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
writeBatch: async (table, rows) => {
|
|
346
|
+
if (rows.length === 0)
|
|
347
|
+
return 0;
|
|
348
|
+
const cols = table.columns;
|
|
349
|
+
const width = cols.length;
|
|
350
|
+
for (const r of rows) {
|
|
351
|
+
if (r.length !== width) {
|
|
352
|
+
throw new Error(`行宽不匹配:期望 ${width} 列,实际 ${r.length} 列(表 ${table.name})`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const colList = cols.map((c) => `"${escIdent(c.name)}"`).join(', ');
|
|
356
|
+
const holders = cols.map((_, i) => `:b${i + 1}`).join(', ');
|
|
357
|
+
const sql = `INSERT INTO ${qual(table.name)} (${colList}) VALUES (${holders})`;
|
|
358
|
+
const conn = await pool.getConnection();
|
|
359
|
+
try {
|
|
360
|
+
await ensureSession(conn, kind);
|
|
361
|
+
let written = 0;
|
|
362
|
+
if (writeMode === 'ignore') {
|
|
363
|
+
// 逐条 try:冲突行跳过(错误码 ORA-00001 / DM -6602 均含 "1" 唯一约束语义,
|
|
364
|
+
// 以消息匹配唯一冲突而非精确码,兼容 DM 差异)
|
|
365
|
+
for (const r of rows) {
|
|
366
|
+
try {
|
|
367
|
+
const res = await conn.execute(sql, r.map((v, i) => toOraBind(v, cols[i])), { autoCommit: false });
|
|
368
|
+
written += res.rowsAffected ?? 0;
|
|
369
|
+
}
|
|
370
|
+
catch (e) {
|
|
371
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
372
|
+
if (/ORA-00001|唯一|unique|UNIQUE|[-\[]6602/.test(msg))
|
|
373
|
+
continue;
|
|
374
|
+
throw e;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else if (writeMode === 'replace') {
|
|
379
|
+
const pkCols = cols.filter((c) => c.primaryKey);
|
|
380
|
+
if (pkCols.length === 0)
|
|
381
|
+
throw new Error(`replace 模式需要主键:表 ${table.name} 无主键`);
|
|
382
|
+
const delSql = `DELETE FROM ${qual(table.name)} WHERE ${pkCols
|
|
383
|
+
.map((c, i) => `"${escIdent(c.name)}" = :d${i + 1}`)
|
|
384
|
+
.join(' AND ')}`;
|
|
385
|
+
for (const r of rows) {
|
|
386
|
+
const keyBind = cols
|
|
387
|
+
.map((c, i) => ({ c, v: r[i] }))
|
|
388
|
+
.filter((x) => x.c.primaryKey)
|
|
389
|
+
.map((x) => toOraBind(x.v ?? null, x.c));
|
|
390
|
+
await conn.execute(delSql, keyBind, { autoCommit: false });
|
|
391
|
+
const res = await conn.execute(sql, r.map((v, i) => toOraBind(v, cols[i])), { autoCommit: false });
|
|
392
|
+
written += res.rowsAffected ?? 0;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// executeMany 单往返批量(node-oracledb/DM 官方批量加载推荐);≤1000 行/次防绑定数组溢出
|
|
397
|
+
for (let i = 0; i < rows.length; i += 1000) {
|
|
398
|
+
const chunk = rows.slice(i, i + 1000).map((r) => r.map((v, i2) => toOraBind(v, cols[i2])));
|
|
399
|
+
const res = await conn.execute(sql, chunk, { autoCommit: false });
|
|
400
|
+
written += res.rowsAffected ?? 0;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
await conn.commit();
|
|
404
|
+
return written;
|
|
405
|
+
}
|
|
406
|
+
catch (e) {
|
|
407
|
+
try {
|
|
408
|
+
await conn.rollback();
|
|
409
|
+
}
|
|
410
|
+
catch { /* 已断开忽略 */ }
|
|
411
|
+
throw e;
|
|
412
|
+
}
|
|
413
|
+
finally {
|
|
414
|
+
await conn.close().catch(() => { });
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
close: async () => {
|
|
418
|
+
await pool.close().catch(() => { });
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/** 默认 owner:fields.user / URL userinfo(oracle/dm 单库多 schema 模型) */
|
|
423
|
+
export function ownerOfFromConn(rc) {
|
|
424
|
+
if (rc.fields && typeof rc.fields.user === 'string' && rc.fields.user !== '')
|
|
425
|
+
return rc.fields.user;
|
|
426
|
+
if (rc.url) {
|
|
427
|
+
try {
|
|
428
|
+
const u = new URL(rc.url);
|
|
429
|
+
if (u.username)
|
|
430
|
+
return decodeURIComponent(u.username);
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
// 解析失败交给上层错误
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
throw new Error(`${rc.meta.kind} 连接缺少用户名(owner)`);
|
|
437
|
+
}
|