@primate/oracledb 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/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) Terrablue <terrablue@proton.me> and contributors.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
@@ -0,0 +1,23 @@
1
+ import type { JSONValue, TypedArray } from "@rcompat/type";
2
+ type Param = Array<unknown> | bigint | boolean | Date | null | number | object | string | TypedArray;
3
+ type Validate<T extends {
4
+ [K in keyof T]: Param;
5
+ }> = T;
6
+ type ColumnTypes = Validate<{
7
+ BOOLEAN: boolean;
8
+ BLOB: TypedArray;
9
+ BINARY_FLOAT: number;
10
+ BINARY_DOUBLE: number;
11
+ NUNBER: number;
12
+ "NUMBER(3)": number;
13
+ "NUMBER(5)": number;
14
+ "NUMBER(10)": number;
15
+ "NUMBER(20)": string;
16
+ "VARCHAR2(36)": string;
17
+ "VARCHAR2(40)": string;
18
+ "VARCHAR2(4000)": string;
19
+ TIMESTAMP: Date;
20
+ JSON: JSONValue;
21
+ }>;
22
+ export type { ColumnTypes as default };
23
+ //# sourceMappingURL=ColumnTypes.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ColumnTypes.js.map
@@ -0,0 +1,60 @@
1
+ import type { As, CheckPlaceholders, DataDict, ExtractPlaceholders, Schema, Sort, SQLDB, With } from "@primate/core/db";
2
+ import type { Dict } from "@rcompat/type";
3
+ import oracledb from "oracledb";
4
+ import type { ObjectType, Parsed } from "pema";
5
+ declare const schema: ObjectType<import("pema").NormalizeSchemaObject<{
6
+ readonly host: import("pema").DefaultType<import("pema").StringType, "localhost">;
7
+ readonly port: import("pema").DefaultType<import("pema").UintType<"u32">, 1521>;
8
+ readonly database: import("pema").StringType;
9
+ readonly username: import("pema").OptionalType<import("pema").StringType>;
10
+ readonly password: import("pema").OptionalType<import("pema").StringType>;
11
+ }>, {
12
+ host: string;
13
+ port: number;
14
+ database: string;
15
+ username: string | undefined;
16
+ password: string | undefined;
17
+ }>;
18
+ export default class OracleDB implements SQLDB<oracledb.Connection> {
19
+ #private;
20
+ static config: typeof schema.input;
21
+ constructor(config?: typeof schema.input, options?: {
22
+ debug?: boolean;
23
+ });
24
+ get client(): oracledb.Connection;
25
+ read(as: As, args: {
26
+ count: true;
27
+ where: DataDict;
28
+ with?: never;
29
+ }): Promise<number>;
30
+ read(as: As, args: {
31
+ where: DataDict;
32
+ fields?: string[];
33
+ limit?: number;
34
+ offset?: number;
35
+ sort?: Sort;
36
+ with?: With;
37
+ }): Promise<Dict[]>;
38
+ close(): Promise<void>;
39
+ get explain(): Dict<{
40
+ query: string;
41
+ }>;
42
+ get schema(): Schema;
43
+ create<O extends Dict>(as: As, record: Dict): Promise<O>;
44
+ update(as: As, args: {
45
+ set: DataDict;
46
+ where: DataDict;
47
+ }): Promise<number>;
48
+ delete(as: As, args: {
49
+ where: DataDict;
50
+ }): Promise<number>;
51
+ sql<Q extends string, I extends ObjectType<{
52
+ [K in ExtractPlaceholders<Q>]: Parsed<unknown>;
53
+ }> | undefined, O extends Parsed<unknown> | undefined>(options: {
54
+ input?: I extends ObjectType<any> ? CheckPlaceholders<I, Q> : I;
55
+ query: Q;
56
+ output?: O;
57
+ }): (args: I extends ObjectType<any> ? I["infer"] : void) => Promise<O extends Parsed<unknown> ? O["infer"] : void>;
58
+ }
59
+ export {};
60
+ //# sourceMappingURL=OracleDB.d.ts.map
@@ -0,0 +1,571 @@
1
+ import typemap from "#typemap";
2
+ import base from "@primate/core/db";
3
+ import E from "@primate/core/db/errors";
4
+ import sql from "@primate/core/db/sql";
5
+ import assert from "@rcompat/assert";
6
+ import is from "@rcompat/is";
7
+ import oracledb from "oracledb";
8
+ import p from "pema";
9
+ const VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
10
+ function quote(name) {
11
+ if (!VALID_IDENTIFIER.test(name))
12
+ throw E.identifier_invalid(name);
13
+ return `"${name}"`;
14
+ }
15
+ function get_column(key) {
16
+ return typemap[key].column;
17
+ }
18
+ async function bind_value(key, value) {
19
+ if (is.null(value))
20
+ return null;
21
+ return typemap[key].bind(value);
22
+ }
23
+ function order_by(sort) {
24
+ if (is.undefined(sort))
25
+ return "";
26
+ const entries = Object.entries(sort);
27
+ if (entries.length === 0)
28
+ return "";
29
+ return ` ORDER BY ${entries.map(([k, dir]) => {
30
+ const direction = dir.toLowerCase() === "desc" ? "DESC" : "ASC";
31
+ return `${quote(k)} ${direction}`;
32
+ }).join(", ")}`;
33
+ }
34
+ function columns_to_types(row) {
35
+ const data_type = row.data_type.toUpperCase();
36
+ const char_length = Number(row.char_length ?? row.data_length ?? 0);
37
+ const precision = is.null(row.data_precision)
38
+ ? null
39
+ : Number(row.data_precision);
40
+ const scale = is.null(row.data_scale) ? 0 : Number(row.data_scale);
41
+ if (data_type === "BLOB")
42
+ return ["blob"];
43
+ if (data_type === "BOOLEAN")
44
+ return ["boolean"];
45
+ if (data_type === "BINARY_FLOAT")
46
+ return ["f32"];
47
+ if (data_type === "BINARY_DOUBLE")
48
+ return ["f64"];
49
+ if (data_type === "JSON")
50
+ return ["json"];
51
+ if (data_type.startsWith("TIMESTAMP"))
52
+ return ["datetime"];
53
+ if (data_type === "VARCHAR2") {
54
+ if (char_length === 36)
55
+ return base.UUID_TYPES;
56
+ if (char_length === 40)
57
+ return ["i128", "u128"];
58
+ if (char_length === 4000)
59
+ return ["string", "time", "url"];
60
+ return ["string"];
61
+ }
62
+ if (data_type === "NUMBER" && scale === 0) {
63
+ if (precision === 3)
64
+ return ["i8", "u8"];
65
+ if (precision === 5)
66
+ return ["i16", "u16"];
67
+ if (precision === 10)
68
+ return ["i32", "u32"];
69
+ if (precision === 20)
70
+ return ["i64", "u64"];
71
+ }
72
+ return [];
73
+ }
74
+ function limit(n) {
75
+ return is.defined(n) ? ` FETCH FIRST ${n} ROWS ONLY` : "";
76
+ }
77
+ function unbind_value(key, value) {
78
+ return typemap[key].unbind(value);
79
+ }
80
+ function unbind(types, row) {
81
+ return sql.unbind(types, row, unbind_value);
82
+ }
83
+ const schema = p({
84
+ host: p.string.default("localhost"),
85
+ port: p.uint.port().default(1521),
86
+ database: p.string,
87
+ username: p.string.optional(),
88
+ password: p.string.optional(),
89
+ });
90
+ export default class OracleDB {
91
+ static config;
92
+ #connection;
93
+ #config;
94
+ #debug = false;
95
+ #explain = {};
96
+ constructor(config, options) {
97
+ this.#config = schema.parse(config);
98
+ this.#debug = options?.debug ?? false;
99
+ }
100
+ get client() {
101
+ return this.#connection;
102
+ }
103
+ async #connect() {
104
+ if (is.undefined(this.#connection)) {
105
+ const { username, password, host, port, database } = this.#config;
106
+ this.#connection = await oracledb.getConnection({
107
+ user: username,
108
+ password,
109
+ connectString: `${host}:${port}/${database}`,
110
+ });
111
+ // return objects as plain dicts with lowercase keys
112
+ this.#connection.callTimeout = 0;
113
+ }
114
+ return this.#connection;
115
+ }
116
+ async #sql(q, params = []) {
117
+ const conn = await this.#connect();
118
+ const EXECUTE_OPTIONS = {
119
+ outFormat: oracledb.OUT_FORMAT_OBJECT,
120
+ // fetch NUMBER columns as strings to preserve bigint precision
121
+ fetchTypeHandler(metadata) {
122
+ if (metadata.dbType === oracledb.DB_TYPE_NUMBER) {
123
+ return { type: oracledb.STRING };
124
+ }
125
+ },
126
+ };
127
+ const result = await conn.execute(q, params, {
128
+ ...EXECUTE_OPTIONS,
129
+ autoCommit: false,
130
+ });
131
+ // lowercase all keys
132
+ return (result.rows ?? []).map((row) => Object.fromEntries(Object.entries(row).map(([k, v]) => [k.toLowerCase(), v])));
133
+ }
134
+ async #where(as, where, index = 1) {
135
+ const keys = Object.keys(where);
136
+ if (keys.length === 0)
137
+ return { WHERE: "", params: [], next_index: index };
138
+ const parts = [];
139
+ const params = [];
140
+ let i = index;
141
+ for (const field of keys) {
142
+ const raw = where[field];
143
+ const datatype = as.types[field];
144
+ const column = quote(field);
145
+ if (is.null(raw)) {
146
+ parts.push(`${column} IS NULL`);
147
+ continue;
148
+ }
149
+ if (is.dict(raw)) {
150
+ const ops = Object.entries(raw);
151
+ if (ops.length === 0)
152
+ throw E.operator_empty(field);
153
+ for (const [op, op_value] of ops) {
154
+ const placeholder = op === "$in" ? "" : `:${i++}`;
155
+ switch (op) {
156
+ case "$like":
157
+ parts.push(`${column} LIKE ${placeholder} ESCAPE '\\'`);
158
+ params.push(await bind_value(datatype, op_value));
159
+ break;
160
+ case "$ilike":
161
+ parts.push(`UPPER(${column}) LIKE UPPER(${placeholder}) ESCAPE '\\'`);
162
+ params.push(await bind_value(datatype, op_value));
163
+ break;
164
+ case "$ne":
165
+ parts.push(`${column} != ${placeholder}`);
166
+ params.push(await bind_value(datatype, op_value));
167
+ break;
168
+ case "$gt":
169
+ case "$gte":
170
+ case "$lt":
171
+ case "$lte":
172
+ case "$after":
173
+ case "$before": {
174
+ const sql_op = sql.OPS[op];
175
+ parts.push(`${column} ${sql_op} ${placeholder}`);
176
+ params.push(await bind_value(datatype, op_value));
177
+ break;
178
+ }
179
+ case "$in": {
180
+ const array = op_value;
181
+ const placeholders = [];
182
+ for (const value of array) {
183
+ placeholders.push(`:${i++}`);
184
+ params.push(await bind_value(datatype, value));
185
+ }
186
+ parts.push(`${column} IN (${placeholders.join(", ")})`);
187
+ break;
188
+ }
189
+ default:
190
+ throw E.operator_unknown(field, op);
191
+ }
192
+ }
193
+ continue;
194
+ }
195
+ parts.push(`${column} = :${i++}`);
196
+ params.push(await bind_value(datatype, raw));
197
+ }
198
+ return {
199
+ WHERE: `WHERE ${parts.join(" AND ")}`,
200
+ params,
201
+ next_index: i,
202
+ };
203
+ }
204
+ async #base_query(as, args) {
205
+ const { fields } = args;
206
+ const SELECT = is.defined(fields) ? fields.map(quote).join(", ") : "*";
207
+ const table = quote(as.table);
208
+ const { WHERE, params } = await this.#where(as, args.where);
209
+ const ORDER_BY = order_by(args.sort);
210
+ const OFFSET = is.defined(args.offset) ? ` OFFSET ${args.offset} ROWS` : "";
211
+ const LIMIT = is.defined(args.limit)
212
+ ? `${OFFSET} FETCH FIRST ${args.limit} ROWS ONLY`
213
+ : "";
214
+ return {
215
+ query: `SELECT ${SELECT} FROM ${table} ${WHERE}${ORDER_BY}${LIMIT}`,
216
+ params,
217
+ };
218
+ }
219
+ async #read(as, args) {
220
+ const { query, params } = await this.#base_query(as, args);
221
+ const rows = await this.#sql(query, params);
222
+ return rows.map(r => unbind(as.types, r));
223
+ }
224
+ async #count(as, where) {
225
+ const { WHERE, params } = await this.#where(as, where);
226
+ const q = `SELECT COUNT(*) AS n FROM ${quote(as.table)} ${WHERE}`;
227
+ const rows = await this.#sql(q, params);
228
+ return Number(rows[0]?.n ?? 0);
229
+ }
230
+ async #read_joined(as, args) {
231
+ if (is.null(as.pk))
232
+ throw E.relation_requires_pk("parent");
233
+ const [[, relation]] = Object.entries(args.with);
234
+ if (is.null(relation.as.pk))
235
+ throw E.relation_requires_pk("target");
236
+ const aliases = sql.aliases([as.table, relation.as.table]);
237
+ const alias = aliases[as.table];
238
+ const r_alias = aliases[relation.as.table];
239
+ const fields = base.fields(args.fields, as.pk) ?? Object.keys(as.types);
240
+ const r_fields = base.fields(relation.fields, relation.fk, relation.as.pk)
241
+ ?? Object.keys(relation.as.types);
242
+ const SELECT = [
243
+ ...fields.map(f => `${alias}.${quote(f)} AS ${alias}_${f}`),
244
+ ...r_fields.map(f => `${r_alias}.${quote(f)} AS ${r_alias}_${f}`),
245
+ ].join(", ");
246
+ const { query, params } = await this.#base_query(as, { ...args, fields });
247
+ const JOIN = `LEFT JOIN ${quote(relation.as.table)} ${r_alias}
248
+ ON ${r_alias}.${quote(relation.fk)} = ${alias}.${quote(as.pk)}`;
249
+ const q = `SELECT ${SELECT} FROM (${query}) ${alias} ${JOIN}`;
250
+ const rows = await this.#sql(q, params);
251
+ return sql.nest(as, { rows, aliases }, args, unbind_value);
252
+ }
253
+ async #read_phased(as, args) {
254
+ const fields = base.expand(as, args.fields, args.with);
255
+ const rows = await this.#read(as, { ...args, fields });
256
+ const out = rows.map(row => base.project(row, args.fields));
257
+ for (const [table, relation] of Object.entries(args.with)) {
258
+ await this.#attach_relation(as, { rows, out, table, relation });
259
+ }
260
+ return out;
261
+ }
262
+ async #attach_relation(as, args) {
263
+ const relation = args.relation;
264
+ const by = relation.reverse === true ? relation.as.pk : relation.fk;
265
+ if (is.null(by))
266
+ throw E.relation_requires_pk("target");
267
+ const parent_by = relation.reverse === true ? relation.fk : as.pk;
268
+ if (is.null(parent_by))
269
+ throw E.relation_requires_pk("parent");
270
+ const join_values = [...new Set(args.rows.map(r => r[parent_by]).filter(v => v != null))];
271
+ const is_many = relation.kind === "many";
272
+ const many = is_many ? [] : null;
273
+ if (join_values.length === 0) {
274
+ for (const row of args.out)
275
+ row[args.table] = many;
276
+ return;
277
+ }
278
+ const related = await this.#load_related({ by, join_values, ...relation });
279
+ const grouped = new Map();
280
+ for (const row of related) {
281
+ const key = row[by];
282
+ if (is.null(key))
283
+ continue;
284
+ if (!grouped.has(key))
285
+ grouped.set(key, []);
286
+ grouped.get(key).push(row);
287
+ }
288
+ for (let i = 0; i < args.out.length; i++) {
289
+ const join_value = args.rows[i][parent_by];
290
+ if (is.null(join_value)) {
291
+ args.out[i][args.table] = is_many ? [] : null;
292
+ continue;
293
+ }
294
+ const rows = grouped.get(join_value) ?? [];
295
+ args.out[i][args.table] = is_many
296
+ ? rows.map(r => base.project(r, relation.fields))
297
+ : rows[0] ? base.project(rows[0], relation.fields) : null;
298
+ }
299
+ }
300
+ async #load_related(args) {
301
+ const per_parent = args.kind === "one" ? 1 : args.limit;
302
+ const { WHERE: _where, params: where_params, next_index } = await this.#where(args.as, args.where);
303
+ const where_part = _where.length > 0 ? _where.slice("WHERE ".length) : "";
304
+ const datatype = args.as.types[args.by];
305
+ let next_i = next_index;
306
+ const in_placeholders = [];
307
+ const in_params = [];
308
+ for (const v of args.join_values) {
309
+ in_placeholders.push(`:${next_i++}`);
310
+ in_params.push(await bind_value(datatype, v));
311
+ }
312
+ const in_part = `${quote(args.by)} IN (${in_placeholders.join(", ")})`;
313
+ const WHERE = is.text(where_part)
314
+ ? `WHERE (${where_part}) AND (${in_part})`
315
+ : `WHERE ${in_part}`;
316
+ const all_columns = Object.keys(args.as.types);
317
+ const fields = args.fields !== undefined && args.fields.length > 0
318
+ ? args.fields
319
+ : all_columns;
320
+ const select_fields = [...new Set([...fields, args.by])];
321
+ const SELECT = select_fields.map(quote).join(", ");
322
+ const ORDER_BY = ` ORDER BY ${quote(args.by)} ASC${is.defined(args.sort)
323
+ ? ", " + Object.entries(args.sort).map(([k, dir]) => `${quote(k)} ${dir.toLowerCase() === "desc" ? "DESC" : "ASC"}`).join(", ")
324
+ : ""}`;
325
+ const table = quote(args.as.table);
326
+ let q;
327
+ if (is.defined(per_parent)) {
328
+ const rn_order = is.defined(args.sort)
329
+ ? Object.entries(args.sort).map(([k, dir]) => `${quote(k)} ${dir.toLowerCase() === "desc" ? "DESC" : "ASC"}`).join(", ")
330
+ : "";
331
+ q = `SELECT ${SELECT} FROM (
332
+ SELECT ${SELECT},
333
+ ROW_NUMBER() OVER (
334
+ PARTITION BY ${quote(args.by)}
335
+ ORDER BY ${quote(args.by)}${is.text(rn_order) ? ", " + rn_order : ""}
336
+ ) AS "__rn"
337
+ FROM ${table}
338
+ ${WHERE}
339
+ )
340
+ WHERE "__rn" <= ${per_parent}
341
+ ${ORDER_BY}`;
342
+ }
343
+ else {
344
+ q = `SELECT ${SELECT} FROM ${table} ${WHERE}${ORDER_BY}`;
345
+ }
346
+ const params = [...where_params, ...in_params];
347
+ const rows = await this.#sql(q, params);
348
+ return rows.map(r => unbind(args.as.types, r));
349
+ }
350
+ async read(as, args) {
351
+ assert.dict(args.where);
352
+ this.#explain = {};
353
+ if (args.count === true)
354
+ return this.#count(as, args.where);
355
+ if (base.withed(args)) {
356
+ return sql.joinable(as, args.with)
357
+ ? this.#read_joined(as, args)
358
+ : this.#read_phased(as, args);
359
+ }
360
+ return this.#read(as, args);
361
+ }
362
+ async close() {
363
+ await this.#connection?.close();
364
+ this.#connection = undefined;
365
+ }
366
+ get explain() {
367
+ return this.#explain;
368
+ }
369
+ get schema() {
370
+ return {
371
+ create: async (table, pk, types) => {
372
+ const columns = [];
373
+ for (const [key, value] of Object.entries(types)) {
374
+ const column_type = get_column(value);
375
+ const column = quote(key);
376
+ const c = `${column} ${column_type}`;
377
+ if (key === pk.name) {
378
+ const is_identity = pk.generate && column_type.startsWith("NUMBER");
379
+ if (is_identity) {
380
+ columns.push(`${c} GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY`);
381
+ }
382
+ else {
383
+ columns.push(`${c} PRIMARY KEY`);
384
+ }
385
+ }
386
+ else {
387
+ columns.push(`${c}`);
388
+ }
389
+ }
390
+ await this.#sql(`CREATE TABLE IF NOT EXISTS ${quote(table)} (${columns.join(", ")})`);
391
+ },
392
+ delete: async (table) => {
393
+ await this.#sql(`DROP TABLE IF EXISTS ${quote(table)} CASCADE CONSTRAINTS PURGE`);
394
+ },
395
+ introspect: async (table /*, pk? */) => {
396
+ const exists = await this.#sql(`
397
+ SELECT COUNT(*) AS n
398
+ FROM USER_TABLES
399
+ WHERE TABLE_NAME = :1
400
+ `, [table]);
401
+ if (Number(exists[0]?.n ?? 0) === 0)
402
+ return null;
403
+ const rows = await this.#sql(`
404
+ SELECT
405
+ COLUMN_NAME,
406
+ DATA_TYPE,
407
+ DATA_LENGTH,
408
+ CHAR_LENGTH,
409
+ DATA_PRECISION,
410
+ DATA_SCALE
411
+ FROM USER_TAB_COLUMNS
412
+ WHERE TABLE_NAME = :1
413
+ ORDER BY COLUMN_ID
414
+ `, [table]);
415
+ const result = {};
416
+ for (const row of rows) {
417
+ const candidates = columns_to_types(row);
418
+ if (candidates.length > 0) {
419
+ result[row.column_name] = candidates;
420
+ }
421
+ }
422
+ return result;
423
+ },
424
+ alter: async (table, diff) => {
425
+ const existing = await this.schema.introspect(table);
426
+ if (is.null(existing))
427
+ throw E.table_not_found(table);
428
+ // rename first
429
+ for (const [from, to] of diff.rename) {
430
+ await this.#sql(`ALTER TABLE ${quote(table)} RENAME COLUMN ${quote(from)} TO ${quote(to)}`);
431
+ }
432
+ // drop next
433
+ for (const field of diff.drop) {
434
+ await this.#sql(`ALTER TABLE ${quote(table)} DROP COLUMN ${quote(field)}`);
435
+ }
436
+ // add last
437
+ for (const [field, type] of Object.entries(diff.add)) {
438
+ await this.#sql(`ALTER TABLE ${quote(table)} ADD (${quote(field)} ${get_column(type)})`);
439
+ }
440
+ },
441
+ };
442
+ }
443
+ async create(as, record) {
444
+ assert.dict(record);
445
+ const pk = as.pk;
446
+ const table = quote(as.table);
447
+ // PK provided or none defined
448
+ if (is.null(pk) || pk in record) {
449
+ const fields = Object.keys(record);
450
+ if (fields.length === 0) {
451
+ await this.#sql(`INSERT INTO ${table} VALUES (DEFAULT)`);
452
+ return record;
453
+ }
454
+ const keys = fields.map(quote).join(", ");
455
+ const values = fields.map((_, i) => `:${i + 1}`).join(", ");
456
+ const params = await this.#create_params(as, record);
457
+ await this.#sql(`INSERT INTO ${table} (${keys}) VALUES (${values})`, params);
458
+ return record;
459
+ }
460
+ // PK missing
461
+ if (as.generate_pk === false)
462
+ throw E.pk_required(pk);
463
+ const type = as.types[pk];
464
+ // UUID — generate client-side
465
+ if (base.is_uuid_type(type)) {
466
+ const pk_value = base.generate_uuid(type);
467
+ const to_insert = { ...record, [pk]: pk_value };
468
+ const fields = Object.keys(to_insert);
469
+ const keys = fields.map(quote).join(", ");
470
+ const values = fields.map((_, i) => `:${i + 1}`).join(", ");
471
+ const params = await this.#create_params(as, to_insert);
472
+ await this.#sql(`INSERT INTO ${table} (${keys}) VALUES (${values})`, params);
473
+ return to_insert;
474
+ }
475
+ // bigint — generate client-side via MAX
476
+ if (base.BIGINT_STRING_TYPES.includes(type)) {
477
+ const q = `SELECT MAX(${quote(pk)}) AS v FROM ${table}`;
478
+ const rows = await this.#sql(q);
479
+ const pk_value = rows[0]?.v !== null && rows[0]?.v !== undefined
480
+ ? BigInt(rows[0].v) + 1n
481
+ : 1n;
482
+ const to_insert = { ...record, [pk]: pk_value };
483
+ const fields = Object.keys(to_insert);
484
+ const keys = fields.map(quote).join(", ");
485
+ const values = fields.map((_, i) => `:${i + 1}`).join(", ");
486
+ const params = await this.#create_params(as, to_insert);
487
+ await this.#sql(`INSERT INTO ${table} (${keys}) VALUES (${values})`, params);
488
+ return to_insert;
489
+ }
490
+ // integer — use GENERATED ALWAYS AS IDENTITY, get PK back via RETURNING INTO
491
+ const fields = Object.keys(record);
492
+ const keys = fields.length > 0 ? fields.map(quote).join(", ") : undefined;
493
+ const values = fields.map((_, i) => `:${i + 1}`).join(", ");
494
+ const params = await this.#create_params(as, record);
495
+ const out_bind = { dir: oracledb.BIND_OUT, type: oracledb.NUMBER };
496
+ const q = fields.length > 0
497
+ ? `INSERT INTO ${table} (${keys}) VALUES (${values}) RETURNING ${quote(pk)} INTO :out`
498
+ : `INSERT INTO ${table} VALUES (DEFAULT) RETURNING ${quote(pk)} INTO :out`;
499
+ const conn = await this.#connect();
500
+ const result = await conn.execute(q, [...params, out_bind], { autoCommit: true });
501
+ const out = result.outBinds[0];
502
+ const pk_raw = Array.isArray(out) ? out[0] : out;
503
+ return { ...record, [pk]: unbind_value(type, pk_raw) };
504
+ }
505
+ #create_params(as, record) {
506
+ return Promise.all(Object.keys(record).map(field => bind_value(as.types[field], record[field])));
507
+ }
508
+ async update(as, args) {
509
+ assert.nonempty(args.set);
510
+ assert.dict(args.where);
511
+ const set_keys = Object.keys(args.set);
512
+ const set_parts = [];
513
+ const set_params = [];
514
+ let i = 1;
515
+ for (const k of set_keys) {
516
+ set_parts.push(`${quote(k)} = :${i++}`);
517
+ set_params.push(await bind_value(as.types[k], args.set[k]));
518
+ }
519
+ const SET = `SET ${set_parts.join(", ")}`;
520
+ const { WHERE, params } = await this.#where(as, args.where, i);
521
+ const q = `UPDATE ${quote(as.table)} ${SET} ${WHERE}`;
522
+ const conn = await this.#connect();
523
+ const result = await conn.execute(q, [...set_params, ...params], {
524
+ autoCommit: true,
525
+ });
526
+ return result.rowsAffected ?? 0;
527
+ }
528
+ async delete(as, args) {
529
+ assert.nonempty(args.where);
530
+ const { WHERE, params } = await this.#where(as, args.where);
531
+ const q = `DELETE FROM ${quote(as.table)} ${WHERE}`;
532
+ const conn = await this.#connect();
533
+ const result = await conn.execute(q, params, { autoCommit: true });
534
+ return result.rowsAffected ?? 0;
535
+ }
536
+ sql(options) {
537
+ if (is.defined(options.input)) {
538
+ const placeholders = new Set([...options.query.matchAll(/:(\w+)/g)].map(m => m[1]));
539
+ const input_keys = new Set(Object.keys(options.input.properties));
540
+ for (const key of input_keys) {
541
+ if (!placeholders.has(key))
542
+ throw E.sql_placeholder_missing(key);
543
+ }
544
+ for (const key of placeholders) {
545
+ if (!input_keys.has(key))
546
+ throw E.sql_input_missing(key);
547
+ }
548
+ }
549
+ return async (args) => {
550
+ const input = is.defined(options.input)
551
+ ? options.input.parse(args)
552
+ : undefined;
553
+ const params = [];
554
+ const name_to_index = new Map();
555
+ const query = options.query.replace(/:(\w+)/g, (_, name) => {
556
+ if (!name_to_index.has(name)) {
557
+ name_to_index.set(name, params.length + 1);
558
+ const types = Object.fromEntries(Object.entries(options.input.properties)
559
+ .map(([k, v]) => [k, v.datatype]));
560
+ params.push(bind_value(types[name], input[name]));
561
+ }
562
+ return `:${name_to_index.get(name)}`;
563
+ });
564
+ const rows = await this.#sql(query, params);
565
+ return (is.defined(options.output)
566
+ ? options.output.parse(rows)
567
+ : undefined);
568
+ };
569
+ }
570
+ }
571
+ //# sourceMappingURL=OracleDB.js.map
@@ -0,0 +1,5 @@
1
+ import type ColumnTypes from "#ColumnTypes";
2
+ import type { TypeMap } from "@primate/core/db";
3
+ declare const typemap: TypeMap<ColumnTypes>;
4
+ export default typemap;
5
+ //# sourceMappingURL=typemap.d.ts.map
@@ -0,0 +1,76 @@
1
+ import oracledb from "oracledb";
2
+ function identity(column) {
3
+ return {
4
+ bind: value => value,
5
+ column,
6
+ unbind: value => value,
7
+ };
8
+ }
9
+ function number(column) {
10
+ return {
11
+ bind: value => value,
12
+ column,
13
+ unbind: value => Number(value),
14
+ };
15
+ }
16
+ const typemap = {
17
+ blob: {
18
+ async bind(value) {
19
+ const arrayBuffer = await value.arrayBuffer();
20
+ return new Uint8Array(arrayBuffer);
21
+ },
22
+ column: "BLOB",
23
+ unbind(value) {
24
+ return new Blob([value], { type: "application/octet-stream" });
25
+ },
26
+ },
27
+ boolean: identity("BOOLEAN"),
28
+ datetime: identity("TIMESTAMP"),
29
+ f32: identity("BINARY_FLOAT"),
30
+ f64: identity("BINARY_DOUBLE"),
31
+ i8: number("NUMBER(3)"),
32
+ i16: number("NUMBER(5)"),
33
+ i32: number("NUMBER(10)"),
34
+ i64: {
35
+ bind(value) { return String(value); },
36
+ column: "NUMBER(20)",
37
+ unbind(value) { return BigInt(value); },
38
+ },
39
+ i128: {
40
+ bind(value) { return String(value); },
41
+ column: "VARCHAR2(40)",
42
+ unbind(value) { return BigInt(value); },
43
+ },
44
+ u8: number("NUMBER(3)"),
45
+ u16: number("NUMBER(5)"),
46
+ u32: number("NUMBER(10)"),
47
+ u64: {
48
+ bind(value) { return String(value); },
49
+ column: "NUMBER(20)",
50
+ unbind(value) { return BigInt(value); },
51
+ },
52
+ u128: {
53
+ bind(value) { return String(value); },
54
+ column: "VARCHAR2(40)",
55
+ unbind(value) { return BigInt(value); },
56
+ },
57
+ string: identity("VARCHAR2(4000)"),
58
+ time: identity("VARCHAR2(4000)"),
59
+ url: {
60
+ bind(value) { return value.toString(); },
61
+ column: "VARCHAR2(4000)",
62
+ unbind(value) { return new URL(value); },
63
+ },
64
+ uuid: identity("VARCHAR2(36)"),
65
+ uuid_v4: identity("VARCHAR2(36)"),
66
+ uuid_v7: identity("VARCHAR2(36)"),
67
+ json: {
68
+ bind(value) {
69
+ return { val: value, type: oracledb.DB_TYPE_JSON };
70
+ },
71
+ column: "JSON",
72
+ unbind(value) { return value; },
73
+ },
74
+ };
75
+ export default typemap;
76
+ //# sourceMappingURL=typemap.js.map
@@ -0,0 +1,4 @@
1
+ import OracleDB from "#OracleDB";
2
+ declare const _default: (config: typeof OracleDB.config) => OracleDB;
3
+ export default _default;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ import OracleDB from "#OracleDB";
2
+ export default (config) => new OracleDB(config);
3
+ //# sourceMappingURL=index.js.map
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@primate/oracledb",
3
+ "version": "0.1.0",
4
+ "description": "OracleDB databases for Primate",
5
+ "homepage": "https://primate.run/docs/database/oracledb",
6
+ "bugs": "https://github.com/primate-run/primate/issues",
7
+ "type": "module",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/primate-run/primate",
12
+ "directory": "packages/oracledb"
13
+ },
14
+ "files": [
15
+ "/lib/**/*.js",
16
+ "/lib/**/*.d.ts",
17
+ "!/**/*.spec.*"
18
+ ],
19
+ "devDependencies": {
20
+ "@rcompat/type": "^0.17.0",
21
+ "@types/oracledb": "^6.10.3"
22
+ },
23
+ "dependencies": {
24
+ "@rcompat/assert": "^0.13.0",
25
+ "@rcompat/is": "^0.11.0",
26
+ "oracledb": "^6.10.0",
27
+ "@primate/core": "^0.7.0",
28
+ "pema": "0.7.0"
29
+ },
30
+ "peerDependencies": {
31
+ "primate": "^0.38.0"
32
+ },
33
+ "imports": {
34
+ "#*": {
35
+ "@primate/source": "./src/private/*.ts",
36
+ "default": "./lib/private/*.js"
37
+ }
38
+ },
39
+ "exports": {
40
+ ".": {
41
+ "@primate/source": "./src/public/index.ts",
42
+ "default": "./lib/public/index.js"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "build": "npm run clean && tsc",
47
+ "clean": "rm -rf ./lib",
48
+ "lint": "eslint .",
49
+ "test": "npm run db && npx proby",
50
+ "db": "scripts/db.sh"
51
+ }
52
+ }