@zerodep-dev/easy-query-ts-oracle 0.0.0-stage → 0.2.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/src/dialect.ts ADDED
@@ -0,0 +1,391 @@
1
+ /** Oracle 方言(不依赖驱动):Oracle 包与 Oracle 兼容的库(达梦)共用 */
2
+ import {
3
+ assembleTables,
4
+ bareName,
5
+ dateFormatParts,
6
+ Dialect,
7
+ SqlWriter,
8
+ standardJsonPath,
9
+ unquoteSql,
10
+ type ColumnOptions,
11
+ type ConflictNode,
12
+ type DbTable,
13
+ type DeleteNode,
14
+ type DialectOptions,
15
+ type FunctionRenderer,
16
+ type InsertNode,
17
+ type Node,
18
+ type QueryRunner,
19
+ type SelectNode,
20
+ type UpdateNode,
21
+ } from '@zerodep-dev/easy-query-ts';
22
+
23
+ /** 支持的服务器版本 */
24
+ export type OracleVersion = '11.2' | '12.1' | '12.2' | '18' | '19' | '21' | '23';
25
+
26
+ /** CASE 分支里参数要转成的类型(按列的种类) */
27
+ const CASE_TYPES: Record<string, string> = {
28
+ int: 'NUMBER',
29
+ bigint: 'NUMBER',
30
+ decimal: 'NUMBER',
31
+ double: 'BINARY_DOUBLE',
32
+ datetime: 'TIMESTAMP(3)',
33
+ date: 'DATE',
34
+ json: 'JSON',
35
+ string: 'VARCHAR2(4000)',
36
+ };
37
+
38
+ const SECONDS: Record<string, number> = { second: 1, minute: 60, hour: 3600, day: 86400 };
39
+
40
+ export class OracleDialect extends Dialect {
41
+ readonly name: string = 'oracle';
42
+ /** CASE 分支里参数要转成的类型(Oracle 兼容的库按自己的类型名覆盖) */
43
+ protected readonly caseTypes: Record<string, string> = CASE_TYPES;
44
+ /** Oracle 一条语句最多 65535 个绑定变量;IN 列表最多 1000 项另由核心切分 */
45
+ override readonly maxBindParams = 65535;
46
+ override readonly indexIfNotExists: boolean;
47
+ override readonly dropIfExists: boolean;
48
+ /** Oracle 的 DDL 会隐式提交 */
49
+ override readonly transactionalDdl = false;
50
+ protected override readonly recursiveKeyword = '';
51
+ protected override readonly ctesAtTop = true;
52
+ /** 23ai 起有 SQL 的 BOOLEAN:条件能当值、布尔列能当条件 */
53
+ protected override readonly booleanValues: boolean;
54
+
55
+ constructor(options: DialectOptions = {}) {
56
+ super(options);
57
+ // 23ai 起有 IF [NOT] EXISTS
58
+ this.indexIfNotExists = this.atLeast('23');
59
+ this.dropIfExists = this.atLeast('23');
60
+ this.booleanValues = this.atLeast('23');
61
+ }
62
+
63
+ override placeholder(index: number): string {
64
+ return `:${index}`;
65
+ }
66
+
67
+ override likeEscape(): string {
68
+ return " ESCAPE '\\'";
69
+ }
70
+
71
+ /**
72
+ * 带毫秒的日期时间文字(核心按 UTC 编码的写法)写成 TO_TIMESTAMP(:n, …):NLS_DATE_FORMAT 认不得小数秒,
73
+ * 放进 DATE 的上下文、CASE 的分支时隐式转换会失败
74
+ */
75
+ override bindParam(value: unknown, w: SqlWriter): string {
76
+ if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}$/.test(value))
77
+ return `TO_TIMESTAMP(${w.bind(this, value)}, 'YYYY-MM-DD HH24:MI:SS.FF3')`;
78
+ return super.bindParam(value, w);
79
+ }
80
+
81
+ /** CASE 的各分支要同一个类型(NULL 的绑定变量默认是 CHAR):参数按列的种类转成那一列的类型 */
82
+ protected override caseValue(n: Node, w: SqlWriter): string {
83
+ const sql = super.caseValue(n, w);
84
+ if (n.k !== 'param' || !n.codec) return sql;
85
+ const type = this.caseTypes[n.codec.kind];
86
+ return type ? `CAST(${sql} AS ${type})` : sql;
87
+ }
88
+
89
+ /** 没有 SQL 的 BOOLEAN 时(23ai 之前)布尔写成 1 / 0 */
90
+ override encodeParam(value: unknown, kind?: string): unknown {
91
+ if (typeof value === 'boolean') return this.booleanValues ? value : value ? 1 : 0;
92
+ return super.encodeParam(value, kind);
93
+ }
94
+
95
+ // ---- 查询 ----
96
+
97
+ /** 12c 起 OFFSET … FETCH */
98
+ override limitOffset(limit: number | undefined, offset: number | undefined): string {
99
+ if (limit === undefined && (offset === undefined || offset === 0)) return '';
100
+ let s = offset ? ` OFFSET ${Math.trunc(offset)} ROWS` : '';
101
+ if (limit !== undefined) s += ` FETCH NEXT ${Math.trunc(limit)} ROWS ONLY`;
102
+ return s;
103
+ }
104
+
105
+ override select(q: SelectNode, w: SqlWriter = new SqlWriter()): string {
106
+ if (!this.atLeast('12.1') && (q.limit !== undefined || (q.offset ?? 0) > 0))
107
+ return this.rownumPage(q, w);
108
+ return super.select(q, w);
109
+ }
110
+
111
+ /** 12c 之前:SELECT 列 FROM (SELECT eq_a.*, ROWNUM eq_rn FROM (原查询) eq_a WHERE ROWNUM <= 上界) WHERE eq_rn > 偏移 */
112
+ private rownumPage(q: SelectNode, w: SqlWriter): string {
113
+ const offset = Math.trunc(q.offset ?? 0);
114
+ const inner = super.select({ ...q, limit: undefined, offset: undefined }, w);
115
+ const bound = q.limit === undefined ? '' : ` WHERE ROWNUM <= ${offset + Math.trunc(q.limit)}`;
116
+ const columns = q.columns.map((c) => this.quote(c.as)).join(', ') || '*';
117
+ return `SELECT ${columns} FROM (SELECT eq_a.*, ROWNUM eq_rn FROM (${inner}) eq_a${bound}) WHERE eq_rn > ${offset} ORDER BY eq_rn`;
118
+ }
119
+
120
+ // ---- 写入 ----
121
+
122
+ /** 表别名不写 AS;带导航的 join 用核心的"按主键自连接"写法(UPDATE … WHERE EXISTS 由子类改写) */
123
+ override update(q: UpdateNode, w: SqlWriter = new SqlWriter()): string {
124
+ const joins = q.joins ?? [];
125
+ if (joins.length === 0) {
126
+ const set = q.set.map((s) => `${this.quote(s.column)} = ${this.value(s.e, w)}`).join(', ');
127
+ let s = `UPDATE ${this.table(q.from.table)} ${q.from.alias} SET ${set}`;
128
+ if (q.where.length > 0) s += ` WHERE ${this.conjunction(q.where, w)}`;
129
+ return s;
130
+ }
131
+ // 没有 UPDATE … FROM:按主键自连接,新值写成多列子查询,范围写成 EXISTS(join 的条件、参数在两处各渲染一次,顺序与文字一致)
132
+ const self = `${q.from.alias}_w`;
133
+ const same = (q.key ?? [])
134
+ .map((k) => `${self}.${this.quote(k)} = ${q.from.alias}.${this.quote(k)}`)
135
+ .join(' AND ');
136
+ if (!same)
137
+ throw new Error(`update through a navigation needs a primary key on ${q.from.table}`);
138
+ const from = () => `${this.table(q.from.table)} ${q.from.alias}${this.joinClauses(joins, w)}`;
139
+ const columns = q.set.map((c) => this.quote(c.column)).join(', ');
140
+ const values = q.set.map((c) => this.value(c.e, w)).join(', ');
141
+ const source = from();
142
+ const scope = from();
143
+ const where = q.where.length > 0 ? ` AND ${this.conjunction(q.where, w)}` : '';
144
+ return `UPDATE ${this.table(q.from.table)} ${self} SET (${columns}) = (SELECT ${values} FROM ${source} WHERE ${same}) WHERE EXISTS (SELECT 1 FROM ${scope} WHERE ${same}${where})`;
145
+ }
146
+
147
+ override delete(q: DeleteNode, w: SqlWriter = new SqlWriter()): string {
148
+ if (!q.joins?.length) {
149
+ let s = `DELETE FROM ${this.table(q.from.table)} ${q.from.alias}`;
150
+ if (q.where.length > 0) s += ` WHERE ${this.conjunction(q.where, w)}`;
151
+ return s;
152
+ }
153
+ const self = `${q.from.alias}_w`;
154
+ const from = `${this.table(q.from.table)} ${q.from.alias}${this.joinClauses(q.joins, w)}`;
155
+ const keys = (q.key ?? []).map(
156
+ (k) => `${self}.${this.quote(k)} = ${q.from.alias}.${this.quote(k)}`,
157
+ );
158
+ const where = [...keys, ...(q.where.length > 0 ? [this.conjunction(q.where, w)] : [])].join(
159
+ ' AND ',
160
+ );
161
+ return `DELETE FROM ${this.table(q.from.table)} ${self} WHERE EXISTS (SELECT 1 FROM ${from} WHERE ${where})`;
162
+ }
163
+
164
+ /**
165
+ * 多行插入写成一个匿名块里的逐行 INSERT(一次往返):INSERT ALL 对自增列只生成一次值,23ai 的多行 VALUES
166
+ * 按第一行推断绑定变量的类型(数字文字进 NUMBER 列会失败)。一块最多 500 行,免得块太大编译慢
167
+ */
168
+ override readonly maxInsertRows: number = 500;
169
+
170
+ /** upsert 写成 MERGE … USING (SELECT … FROM DUAL) */
171
+ override insert(q: InsertNode, w: SqlWriter = new SqlWriter()): string {
172
+ if (q.conflict) return this.merge(q, q.conflict, w);
173
+ if (q.source || q.rows.length === 1) return super.insert(q, w);
174
+ const rows = q.rows.map((r) => super.insert({ ...q, rows: [r] }, w));
175
+ return `BEGIN ${rows.join('; ')}; END;`;
176
+ }
177
+
178
+ private merge(q: InsertNode, conflict: ConflictNode, w: SqlWriter): string {
179
+ const cols = q.columns.map((c) => this.quote(c));
180
+ const source = q.rows
181
+ .map(
182
+ (r) => `SELECT ${r.map((n, i) => `${this.node(n, w)} AS ${cols[i]}`).join(', ')} FROM DUAL`,
183
+ )
184
+ .join(' UNION ALL ');
185
+ const on = conflict.target
186
+ .map((t) => `eq_t.${this.quote(t)} = eq_s.${this.quote(t)}`)
187
+ .join(' AND ');
188
+ let s = `MERGE INTO ${this.table(q.table)} eq_t USING (${source}) eq_s ON (${on})`;
189
+ if (conflict.update.length > 0)
190
+ s += ` WHEN MATCHED THEN UPDATE SET ${conflict.update.map((u) => `eq_t.${this.quote(u)} = eq_s.${this.quote(u)}`).join(', ')}`;
191
+ return `${s} WHEN NOT MATCHED THEN INSERT (${cols.join(', ')}) VALUES (${cols.map((c) => `eq_s.${c}`).join(', ')})`;
192
+ }
193
+
194
+ /** 单行插入取自增主键:RETURNING 主键 INTO 输出变量(驱动认 :eq_id) */
195
+ override readonly generatedKeys = 'returning';
196
+
197
+ override returning(column: string): string {
198
+ return ` RETURNING ${this.quote(column)} INTO :eq_id`;
199
+ }
200
+
201
+ override releaseSavepoint(): string | undefined {
202
+ return undefined;
203
+ }
204
+
205
+ // ---- 函数 ----
206
+
207
+ protected override functions(): Record<string, FunctionRenderer> {
208
+ return {
209
+ ...super.functions(),
210
+ length: (a) => `LENGTH(${a[0]})`,
211
+ concat: (a) => `(${a.map((x) => `${x}`).join(' || ')})`,
212
+ substring: (a) => `SUBSTR(${a.join(', ')})`,
213
+ index_of: (a) => `(INSTR(${a[0]}, ${a[1]}) - 1)`,
214
+ left: (a) => `SUBSTR(${a[0]}, 1, ${a[1]})`,
215
+ right: (a) => `SUBSTR(${a[0]}, -LEAST(${a[1]}, LENGTH(${a[0]})))`,
216
+ ceil: (a) => `CEIL(${a[0]})`,
217
+ log10: (a) => `LOG(10, ${a[0]})`,
218
+ log: (a) => `LOG(${a[1]}, ${a[0]})`,
219
+ atan2: (a) => `ATAN2(${a[0]}, ${a[1]})`,
220
+ add_days: (a) => `(${a[0]} + NUMTODSINTERVAL(${a[1]}, 'DAY'))`,
221
+ add_months: (a) => `ADD_MONTHS(${a[0]}, ${a[1]})`,
222
+ diff_days: (a) => `(TRUNC(CAST(${a[0]} AS DATE)) - TRUNC(CAST(${a[1]} AS DATE)))`,
223
+ // 年、月是 YEAR TO MONTH 的区间,其余是 DAY TO SECOND
224
+ add_time: (a) => {
225
+ const unit = unquoteSql(a[2]!).toUpperCase();
226
+ const fn = unit === 'YEAR' || unit === 'MONTH' ? 'NUMTOYMINTERVAL' : 'NUMTODSINTERVAL';
227
+ return `(${a[0]} + ${fn}(${a[1]}, '${unit}'))`;
228
+ },
229
+ diff_time: (a) =>
230
+ `TRUNC((CAST(${a[0]} AS DATE) - CAST(${a[1]} AS DATE)) * 86400 / ${SECONDS[unquoteSql(a[2]!)]})`,
231
+ hour: (a) => `EXTRACT(HOUR FROM CAST(${a[0]} AS TIMESTAMP))`,
232
+ minute: (a) => `EXTRACT(MINUTE FROM CAST(${a[0]} AS TIMESTAMP))`,
233
+ second: (a) => `TRUNC(EXTRACT(SECOND FROM CAST(${a[0]} AS TIMESTAMP)))`,
234
+ day_of_year: (a) => `TO_NUMBER(TO_CHAR(${a[0]}, 'DDD'))`,
235
+ // 与 NLS_TERRITORY 无关的星期:相对一个已知的星期日算
236
+ day_of_week: (a) => `MOD(TRUNC(${a[0]}) - DATE '1970-01-04', 7)`,
237
+ iso_day_of_week: (a) => `(MOD(TRUNC(${a[0]}) - DATE '1970-01-05', 7) + 1)`,
238
+ date_format: (a) => this.formatDate(a[0]!, dateFormatParts(unquoteSql(a[1]!))),
239
+ date_trunc: (a) => this.dateTrunc(a[0]!, unquoteSql(a[1]!)),
240
+ now: () => `SYS_EXTRACT_UTC(SYSTIMESTAMP)`,
241
+ in_csv: (a) => `(',' || ${a[1]} || ',' LIKE '%,' || ${a[0]} || ',%')`,
242
+ truncate: (a) => `TRUNC(${a[0]}, ${a[1]})`,
243
+ // 正则(Expr.matches):第三个参数 'c' 区分大小写、'i' 忽略
244
+ regex_match: (a) => `REGEXP_LIKE(${a[0]}, ${a[1]}, ${a[2]})`,
245
+ json_value: (a) => `JSON_QUERY(${a[0]}, ${standardJsonPath(a[1]!)} WITH CONDITIONAL WRAPPER)`,
246
+ json_text: (a) => `JSON_VALUE(${a[0]}, ${standardJsonPath(a[1]!)})`,
247
+ json_number: (a) => `JSON_VALUE(${a[0]}, ${standardJsonPath(a[1]!)} RETURNING BINARY_DOUBLE)`,
248
+ json_boolean: (a) =>
249
+ `(CASE JSON_VALUE(${a[0]}, ${standardJsonPath(a[1]!)}) WHEN 'true' THEN 1 WHEN 'false' THEN 0 END)`,
250
+ json_has: (a) => `JSON_EXISTS(${a[0]}, ${standardJsonPath(a[1]!)})`,
251
+ json_length: (a) =>
252
+ `JSON_VALUE(${a[0]}, ${standardJsonPath(a[1]!).replace(/'$/, ".size()'")})`,
253
+ cast_string: (a) => `CAST(${a[0]} AS VARCHAR2(4000))`,
254
+ cast_int: (a) => `CAST(${a[0]} AS NUMBER(10))`,
255
+ cast_bigint: (a) => `CAST(${a[0]} AS NUMBER(19))`,
256
+ cast_double: (a) => `CAST(${a[0]} AS BINARY_DOUBLE)`,
257
+ cast_datetime: (a) => `CAST(${a[0]} AS TIMESTAMP(3))`,
258
+ };
259
+ }
260
+
261
+ private dateTrunc(x: string, unit: string): string {
262
+ const format = {
263
+ year: 'YYYY',
264
+ quarter: 'Q',
265
+ month: 'MM',
266
+ week: 'IW',
267
+ day: 'DD',
268
+ hour: 'HH24',
269
+ minute: 'MI',
270
+ }[unit];
271
+ if (!format) return `CAST(CAST(${x} AS DATE) AS TIMESTAMP(3))`;
272
+ return `CAST(TRUNC(${x}, '${format}') AS TIMESTAMP(3))`;
273
+ }
274
+
275
+ /** LISTAGG(e, 分隔符) WITHIN GROUP (ORDER BY …);DISTINCT 要 19c */
276
+ protected override stringAgg(n: Extract<Node, { k: 'string_agg' }>, w: SqlWriter): string {
277
+ if (n.distinct) this.requires('LISTAGG(DISTINCT …)', '19');
278
+ const order = this.aggOrder(n.order, w).trim() || 'ORDER BY NULL';
279
+ return `LISTAGG(${n.distinct ? 'DISTINCT ' : ''}${this.node(n.e, w)}, '${n.separator.replaceAll("'", "''")}') WITHIN GROUP (${order})`;
280
+ }
281
+
282
+ // ---- 建表 ----
283
+
284
+ override columnType(kind: string, o: ColumnOptions): string {
285
+ if (o.dbType) return o.dbType;
286
+ switch (kind) {
287
+ case 'string':
288
+ return `VARCHAR2(${o.length ?? 255} CHAR)`;
289
+ case 'int':
290
+ return 'NUMBER(10)';
291
+ case 'bigint':
292
+ return 'NUMBER(19)';
293
+ case 'decimal':
294
+ return `NUMBER(${o.precision ?? 38}, ${o.scale ?? 10})`;
295
+ case 'double':
296
+ return 'BINARY_DOUBLE';
297
+ case 'boolean':
298
+ return this.atLeast('23') ? 'BOOLEAN' : 'NUMBER(1)';
299
+ case 'time':
300
+ return 'VARCHAR2(12)';
301
+ case 'datetime':
302
+ return 'TIMESTAMP(3)';
303
+ case 'json':
304
+ return this.atLeast('21') ? 'JSON' : 'CLOB';
305
+ case 'bytes':
306
+ return 'BLOB';
307
+ default:
308
+ return super.columnType(kind, o);
309
+ }
310
+ }
311
+
312
+ override createTableStart(ifNotExists: boolean): string {
313
+ return ifNotExists && this.atLeast('23') ? 'CREATE TABLE IF NOT EXISTS' : 'CREATE TABLE';
314
+ }
315
+
316
+ /** DEFAULT 要写在 NOT NULL 前面 */
317
+ override columnDefinition(name: string, kind: string, o: ColumnOptions): string {
318
+ return [
319
+ this.quote(name),
320
+ this.columnType(kind, o),
321
+ o.generated ? this.identity() : undefined,
322
+ o.dbDefault !== undefined ? `DEFAULT ${o.dbDefault}` : undefined,
323
+ o.nullable ? undefined : 'NOT NULL',
324
+ ]
325
+ .filter(Boolean)
326
+ .join(' ');
327
+ }
328
+
329
+ override identity(): string {
330
+ this.requires('IDENTITY columns', '12.1');
331
+ return 'GENERATED BY DEFAULT ON NULL AS IDENTITY';
332
+ }
333
+
334
+ override addColumn(table: string, name: string, kind: string, o: ColumnOptions): string {
335
+ return `ALTER TABLE ${this.table(table)} ADD (${this.columnDefinition(name, kind, o)})`;
336
+ }
337
+
338
+ /** 读库结构时列类型的写法(user_tab_columns 别名 c),与 columnType 的输出对得上 */
339
+ protected readonly introspectedType: string = `LOWER(c.data_type) || CASE
340
+ WHEN c.data_type IN ('VARCHAR2', 'NVARCHAR2', 'CHAR', 'NCHAR') THEN '(' || c.char_length || CASE c.char_used WHEN 'C' THEN ' char' ELSE '' END || ')'
341
+ WHEN c.data_type = 'NUMBER' AND c.data_precision IS NOT NULL THEN '(' || c.data_precision || CASE WHEN c.data_scale > 0 THEN ', ' || c.data_scale ELSE '' END || ')'
342
+ ELSE '' END`;
343
+
344
+ /** 读库结构时判断自增列(1 / 0) */
345
+ protected readonly introspectedIdentity: string = `CASE c.identity_column WHEN 'YES' THEN 1 ELSE 0 END`;
346
+
347
+ /** USER_* 数据字典(当前用户的表) */
348
+ override async introspect(run: QueryRunner): Promise<DbTable[]> {
349
+ return assembleTables({
350
+ tables: await run(
351
+ `SELECT t.table_name AS "table_name", c.comments AS "comment" FROM user_tables t
352
+ LEFT JOIN user_tab_comments c ON c.table_name = t.table_name ORDER BY t.table_name`,
353
+ ),
354
+ columns: await run(
355
+ `SELECT c.table_name AS "table_name", c.column_name AS "column_name",
356
+ ${this.introspectedType} AS "column_type",
357
+ CASE c.nullable WHEN 'Y' THEN 1 ELSE 0 END AS "is_nullable", c.data_default AS "column_default",
358
+ m.comments AS "comment", ${this.introspectedIdentity} AS "is_generated",
359
+ CASE WHEN EXISTS (SELECT 1 FROM user_constraints k JOIN user_cons_columns kc ON kc.constraint_name = k.constraint_name
360
+ WHERE k.constraint_type = 'P' AND k.table_name = c.table_name AND kc.column_name = c.column_name) THEN 1 ELSE 0 END AS "primary_key"
361
+ FROM user_tab_columns c
362
+ LEFT JOIN user_col_comments m ON m.table_name = c.table_name AND m.column_name = c.column_name
363
+ ORDER BY c.table_name, c.column_id`,
364
+ ),
365
+ indexes: await run(
366
+ `SELECT i.table_name AS "table_name", i.index_name AS "index_name", CASE i.uniqueness WHEN 'UNIQUE' THEN 1 ELSE 0 END AS "is_unique",
367
+ ic.column_name AS "column_name"
368
+ FROM user_indexes i JOIN user_ind_columns ic ON ic.index_name = i.index_name
369
+ WHERE NOT EXISTS (SELECT 1 FROM user_constraints k WHERE k.index_name = i.index_name AND k.constraint_type = 'P')
370
+ ORDER BY i.table_name, i.index_name, ic.column_position`,
371
+ ),
372
+ foreignKeys: await run(
373
+ `SELECT k.table_name AS "table_name", k.constraint_name AS "constraint_name", kc.column_name AS "column_name",
374
+ r.table_name AS "referenced_table", rc.column_name AS "referenced_column"
375
+ FROM user_constraints k JOIN user_cons_columns kc ON kc.constraint_name = k.constraint_name
376
+ JOIN user_constraints r ON r.constraint_name = k.r_constraint_name
377
+ JOIN user_cons_columns rc ON rc.constraint_name = r.constraint_name AND rc.position = kc.position
378
+ WHERE k.constraint_type = 'R' ORDER BY k.table_name, k.constraint_name, kc.position`,
379
+ ),
380
+ });
381
+ }
382
+
383
+ /** varchar2(50 char) 与 VARCHAR2(50 CHAR)、number(10) 等写法不算差异 */
384
+ protected override normalizeType(type: string): string {
385
+ return super.normalizeType(type);
386
+ }
387
+
388
+ override renameTable(from: string, to: string): string {
389
+ return `ALTER TABLE ${this.table(from)} RENAME TO ${this.quote(bareName(to))}`;
390
+ }
391
+ }
package/src/index.ts ADDED
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Oracle Database,驱动用 oracledb 的 Thin 模式(纯 JS,不装 Instant Client)。按服务器版本渲染:
3
+ * ```ts
4
+ * import oracledb from 'oracledb'
5
+ * const pool = await oracledb.createPool({ user, password, connectString: 'host:1521/FREEPDB1' })
6
+ * const db = createDb({ schema, adapter: oracle(pool) }) // 默认按最新(23ai)
7
+ * const old = createDb({ schema, adapter: oracle(pool, { version: '11.2' }) }) // ROWNUM 分页、NUMBER(1) 布尔
8
+ * ```
9
+ * 版本门槛(低于时退回旧写法,没有旧写法就报错并说明要哪个版本):
10
+ * - 12.1:OFFSET … FETCH 分页(之前用 ROWNUM 包两层)、IDENTITY 列、LISTAGG 之外的 JSON_VALUE 等
11
+ * - 23:SQL 的 BOOLEAN(条件能直接当值、布尔列能直接当条件、BOOLEAN 列类型)、多行 VALUES、IF [NOT] EXISTS
12
+ * 与其它库的差异:表别名不写 AS;参数是 :1、:2;空字符串就是 NULL;没有 TIME 类型(存成 VARCHAR2(12) 的文字)。
13
+ * 会话的日期格式设成 ISO,日期、时间戳按文字读出再由列的 codec 解码(与 MySQL、PG 的口径一致,不受 Node 的本地时区影响)。
14
+ */
15
+ import oracledb from 'oracledb';
16
+ import {
17
+ type Adapter,
18
+ type Without,
19
+ type Driver,
20
+ type ExecuteResult,
21
+ type Session,
22
+ type TransactionOptions,
23
+ } from '@zerodep-dev/easy-query-ts';
24
+ import { OracleDialect, type OracleVersion } from './dialect.ts';
25
+
26
+ export { OracleDialect, type OracleVersion } from './dialect.ts';
27
+
28
+ // ---- 驱动 ----
29
+
30
+ const SESSION_FORMATS = [
31
+ `ALTER SESSION SET NLS_DATE_FORMAT = 'YYYY-MM-DD HH24:MI:SS'`,
32
+ `ALTER SESSION SET NLS_TIMESTAMP_FORMAT = 'YYYY-MM-DD HH24:MI:SS.FF3'`,
33
+ `ALTER SESSION SET TIME_ZONE = '+00:00'`,
34
+ ];
35
+
36
+ /** 驱动按本地时区造出的 Date → 'YYYY-MM-DD HH:MM:SS.mmm'(库里存的不带时区的时刻,核心按 UTC 读) */
37
+ function wallClock(d: Date): string {
38
+ const p = (n: number, w = 2) => String(n).padStart(w, '0');
39
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}.${p(d.getMilliseconds(), 3)}`;
40
+ }
41
+
42
+ /** 读出的值:日期、数字、大文本按文字(数字精度、日期时区都不经过 JS 的转换),二进制按 Buffer */
43
+ function fetchInfo(): oracledb.ExecuteOptions {
44
+ return {
45
+ outFormat: oracledb.OUT_FORMAT_OBJECT,
46
+ fetchTypeHandler: (meta) => {
47
+ switch (meta.dbType) {
48
+ // 不带时区的 DATE / TIMESTAMP:驱动按 Node 的本地时区造 Date,取回本地的各部分就是库里存的墙上时间
49
+ case oracledb.DB_TYPE_DATE:
50
+ case oracledb.DB_TYPE_TIMESTAMP:
51
+ return { converter: (v: unknown) => (v instanceof Date ? wallClock(v) : null) };
52
+ case oracledb.DB_TYPE_TIMESTAMP_TZ:
53
+ case oracledb.DB_TYPE_TIMESTAMP_LTZ:
54
+ return { converter: (v: unknown) => (v instanceof Date ? v.toISOString() : null) };
55
+ case oracledb.DB_TYPE_NUMBER:
56
+ case oracledb.DB_TYPE_CLOB:
57
+ case oracledb.DB_TYPE_NCLOB:
58
+ return { type: oracledb.STRING };
59
+ case oracledb.DB_TYPE_BLOB:
60
+ return { type: oracledb.BUFFER };
61
+ case oracledb.DB_TYPE_JSON:
62
+ return { converter: (v: unknown) => (v === null ? null : JSON.stringify(v)) };
63
+ default:
64
+ return undefined;
65
+ }
66
+ },
67
+ };
68
+ }
69
+
70
+ class OracleSession implements Session {
71
+ protected readonly connection: () => Promise<{
72
+ conn: oracledb.Connection;
73
+ release: () => Promise<void>;
74
+ }>;
75
+ private readonly autoCommit: boolean;
76
+
77
+ constructor(
78
+ connection: () => Promise<{ conn: oracledb.Connection; release: () => Promise<void> }>,
79
+ autoCommit: boolean,
80
+ ) {
81
+ this.connection = connection;
82
+ this.autoCommit = autoCommit;
83
+ }
84
+
85
+ async query(sql: string, params: readonly unknown[]): Promise<Record<string, unknown>[]> {
86
+ const { conn, release } = await this.connection();
87
+ try {
88
+ // INSERT … RETURNING 主键 INTO :eq_id:取输出变量当成一行返回
89
+ if (/\bINTO :eq_id$/.test(sql)) {
90
+ const result = await conn.execute(
91
+ sql,
92
+ [...params.map(bindValue), { dir: oracledb.BIND_OUT, type: oracledb.STRING }],
93
+ {
94
+ autoCommit: this.autoCommit,
95
+ },
96
+ );
97
+ const out = result.outBinds as unknown[];
98
+ return [{ eq_id: (out[out.length - 1] as unknown[])[0] }];
99
+ }
100
+ const result = await conn.execute<Record<string, unknown>>(
101
+ sql,
102
+ params.map(bindValue),
103
+ fetchInfo(),
104
+ );
105
+ return result.rows ?? [];
106
+ } finally {
107
+ await release();
108
+ }
109
+ }
110
+
111
+ async execute(sql: string, params: readonly unknown[]): Promise<ExecuteResult> {
112
+ const { conn, release } = await this.connection();
113
+ try {
114
+ const result = await conn.execute(sql, params.map(bindValue), {
115
+ autoCommit: this.autoCommit,
116
+ });
117
+ // 方言把多行插入写成匿名块里的逐行 INSERT:块不报告行数,成功就是每行一行
118
+ if (/^BEGIN INSERT INTO /.test(sql))
119
+ return { affected: sql.match(/\bINSERT INTO /g)!.length, insertId: undefined };
120
+ return { affected: result.rowsAffected ?? 0, insertId: undefined };
121
+ } finally {
122
+ await release();
123
+ }
124
+ }
125
+ }
126
+
127
+ /** JS 值 → 绑定值:bigint 写成文字,Uint8Array 写成 Buffer */
128
+ function bindValue(value: unknown): unknown {
129
+ if (typeof value === 'bigint') return value.toString();
130
+ if (value instanceof Uint8Array && !Buffer.isBuffer(value)) return Buffer.from(value);
131
+ return value;
132
+ }
133
+
134
+ const ISOLATION = {
135
+ 'read uncommitted': 'READ COMMITTED',
136
+ 'read committed': 'READ COMMITTED',
137
+ 'repeatable read': 'SERIALIZABLE',
138
+ serializable: 'SERIALIZABLE',
139
+ } as const;
140
+
141
+ class OracleDriver extends OracleSession implements Driver {
142
+ private readonly pool: oracledb.Pool;
143
+
144
+ constructor(pool: oracledb.Pool) {
145
+ super(async () => {
146
+ const conn = await pool.getConnection();
147
+ return { conn, release: () => conn.close() };
148
+ }, true);
149
+ this.pool = pool;
150
+ }
151
+
152
+ async transaction<T>(
153
+ work: (session: Session) => Promise<T>,
154
+ options?: TransactionOptions,
155
+ ): Promise<T> {
156
+ const conn = await this.pool.getConnection();
157
+ try {
158
+ if (options?.readOnly) await conn.execute('SET TRANSACTION READ ONLY');
159
+ else if (options?.isolation)
160
+ await conn.execute(`SET TRANSACTION ISOLATION LEVEL ${ISOLATION[options.isolation]}`);
161
+ try {
162
+ const result = await work(
163
+ new OracleSession(async () => ({ conn, release: async () => undefined }), false),
164
+ );
165
+ await conn.commit();
166
+ return result;
167
+ } catch (error) {
168
+ await conn.rollback();
169
+ throw error;
170
+ }
171
+ } finally {
172
+ await conn.close();
173
+ }
174
+ }
175
+ }
176
+
177
+ export interface OracleOptions {
178
+ /** 服务器版本:按它选写法,默认最新(23ai) */
179
+ readonly version?: OracleVersion;
180
+ }
181
+
182
+ /**
183
+ * 连接池:建池时给 sessionCallback 设好会话的日期格式与时区(见 oracleSession),
184
+ * `oracledb.createPool({ …, sessionCallback: oracleSession })`
185
+ */
186
+ export function oracle(
187
+ pool: oracledb.Pool,
188
+ options: OracleOptions = {},
189
+ ): Adapter<Without<'returning'>> {
190
+ return {
191
+ dialect: new OracleDialect({ version: options.version }),
192
+ driver: new OracleDriver(pool),
193
+ };
194
+ }
195
+
196
+ /** 连上库读出服务器版本(如 23.5.0.24.7)再建 adapter,部署到 11g 还是 23ai 都自动选对写法 */
197
+ oracle.detect = async (pool: oracledb.Pool): Promise<Adapter<Without<'returning'>>> => {
198
+ const conn = await pool.getConnection();
199
+ try {
200
+ return {
201
+ dialect: new OracleDialect({ version: conn.oracleServerVersionString }),
202
+ driver: new OracleDriver(pool),
203
+ };
204
+ } finally {
205
+ await conn.close();
206
+ }
207
+ };
208
+
209
+ /** 建池时的 sessionCallback:日期、时间戳的会话格式设成 ISO、时区 UTC(核心按 UTC 文字读写日期时间) */
210
+ export function oracleSession(
211
+ conn: oracledb.Connection,
212
+ _requested: string,
213
+ done: (error?: Error) => void,
214
+ ): void {
215
+ // 驱动要的是回调形式:依次执行,完成或出错时调 done
216
+ SESSION_FORMATS.reduce<Promise<unknown>>(
217
+ (prev, sql) => prev.then(() => conn.execute(sql)),
218
+ Promise.resolve(),
219
+ ).then(
220
+ () => done(),
221
+ (error: unknown) => done(error as Error),
222
+ );
223
+ }