@mikro-orm/oracledb 7.1.0-dev.43 → 7.1.0-dev.44

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.
@@ -1,12 +1,21 @@
1
1
  import { AbstractSqlConnection, type AnyEntity, type EntityData, type LoggingOptions, NativeQueryBuilder, OracleDialect, type QueryResult, type RawQueryFragment, type Transaction } from '@mikro-orm/sql';
2
+ import type { Routine } from '@mikro-orm/core';
2
3
  import { type PoolAttributes } from 'oracledb';
3
- /** Oracle database connection using the `oracledb` driver. */
4
4
  export declare class OracleConnection extends AbstractSqlConnection {
5
+ private oraclePool?;
6
+ private acquireOracleConnection?;
5
7
  createKyselyDialect(overrides: PoolAttributes): Promise<OracleDialect>;
6
8
  mapOptions(overrides: PoolAttributes): PoolAttributes;
7
9
  execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
8
10
  /** @inheritDoc */
9
11
  executeDump(dump: string): Promise<void>;
12
+ /**
13
+ * Oracle routines acquire their own pool connection with `autoCommit: true` (refcursor binds
14
+ * and DML need to resolve in one round-trip), so they cannot share the EM's transaction. Wrapping
15
+ * a writing routine in `em.transactional(...)` would silently auto-commit its writes. Read-only
16
+ * functions are allowed through.
17
+ */
18
+ callRoutine<T>(routine: Routine, args?: Record<string, unknown>, ctx?: Transaction): Promise<T>;
10
19
  private stripTrailingSemicolon;
11
20
  protected transformRawResult<T>(res: any, method: 'all' | 'get' | 'run'): T;
12
21
  }
@@ -1,8 +1,31 @@
1
1
  import { AbstractSqlConnection, isRaw, NativeQueryBuilder, OracleDialect, Utils, } from '@mikro-orm/sql';
2
2
  import { CompiledQuery } from 'kysely';
3
3
  import oracledb from 'oracledb';
4
- /** Oracle database connection using the `oracledb` driver. */
4
+ /**
5
+ * Maps a routine's declared runtime type to an oracledb bind descriptor. Delegates the
6
+ * runtime → DB_TYPE mapping to `OraclePlatform.mapToOracleType`; only the VARCHAR/RAW out-binds
7
+ * need an extra `maxSize` hint.
8
+ */
9
+ function oracleBindTypeFromRuntime(platform, runtime) {
10
+ const type = platform.mapToOracleType(runtime ?? 'string');
11
+ return type === oracledb.DB_TYPE_VARCHAR || type === oracledb.DB_TYPE_RAW ? { type, maxSize: 4000 } : { type };
12
+ }
13
+ function oracleReturnBind(platform, routine) {
14
+ const returns = routine.returns;
15
+ const runtime = returns && typeof returns === 'object' && 'runtimeType' in returns ? returns.runtimeType : undefined;
16
+ return { dir: oracledb.BIND_OUT, ...oracleBindTypeFromRuntime(platform, runtime) };
17
+ }
18
+ /** Read-only functions can run on a separate Oracle connection inside an EM transaction — write-ish routines would silently auto-commit. */
19
+ function isReadOnlyRoutine(routine) {
20
+ if (routine.type !== 'function') {
21
+ return false;
22
+ }
23
+ return routine.dataAccess === 'no-sql' || routine.dataAccess === 'reads-sql-data';
24
+ }
5
25
  export class OracleConnection extends AbstractSqlConnection {
26
+ oraclePool;
27
+ // Resolves `password` callbacks per acquire so `callRoutine` doesn't bypass password rotation.
28
+ acquireOracleConnection;
6
29
  async createKyselyDialect(overrides) {
7
30
  const options = this.mapOptions(overrides);
8
31
  const password = options.password;
@@ -19,6 +42,7 @@ export class OracleConnection extends AbstractSqlConnection {
19
42
  for (let attempt = 1;; attempt++) {
20
43
  try {
21
44
  pool = await oracledb.createPool(poolOptions);
45
+ this.oraclePool = pool;
22
46
  break;
23
47
  /* v8 ignore start: transient Oracle pool-creation errors are not reproducible in tests */
24
48
  }
@@ -48,13 +72,13 @@ export class OracleConnection extends AbstractSqlConnection {
48
72
  return undefined;
49
73
  },
50
74
  };
51
- // When password is a callback, wrap the pool to resolve it per-connection.
52
- // oracledb supports per-connection password override via getConnection({ password }).
75
+ this.acquireOracleConnection =
76
+ typeof password === 'function'
77
+ ? async () => pool.getConnection({ password: await password() })
78
+ : () => pool.getConnection();
53
79
  const wrappedPool = typeof password === 'function'
54
80
  ? {
55
- async getConnection() {
56
- return pool.getConnection({ password: await password() });
57
- },
81
+ getConnection: this.acquireOracleConnection,
58
82
  close: (drainTime) => pool.close(drainTime),
59
83
  }
60
84
  : pool;
@@ -133,6 +157,93 @@ export class OracleConnection extends AbstractSqlConnection {
133
157
  }
134
158
  }
135
159
  }
160
+ /**
161
+ * Oracle routines acquire their own pool connection with `autoCommit: true` (refcursor binds
162
+ * and DML need to resolve in one round-trip), so they cannot share the EM's transaction. Wrapping
163
+ * a writing routine in `em.transactional(...)` would silently auto-commit its writes. Read-only
164
+ * functions are allowed through.
165
+ */
166
+ async callRoutine(routine, args = {}, ctx) {
167
+ /* v8 ignore next 3 */
168
+ if (!this.oraclePool || !this.acquireOracleConnection) {
169
+ throw new Error('Oracle pool not initialised — call connect() before callRoutine().');
170
+ }
171
+ if (ctx && !isReadOnlyRoutine(routine)) {
172
+ throw new Error(`Routine ${routine.name} was invoked inside an EntityManager transaction, but Oracle's callRoutine runs on its own pool connection with autoCommit. Read-only functions are allowed through; for procedures or routines that may write, call em.callRoutine() outside em.transactional(), or mark the routine as 'dataAccess: \\'reads-sql-data\\'' / 'no-sql' if it really is read-only.`);
173
+ }
174
+ const name = routine.name.toUpperCase();
175
+ // Cross-schema needs an `OWNER.NAME` prefix; mirror the schema helper's quoting on both branches.
176
+ const quotedName = this.platform.quoteIdentifier(name);
177
+ /* v8 ignore next 3 — cross-schema invocation isn't reachable from the single-user test DB;
178
+ the qualified DDL path (createRoutine/dropRoutine) is covered by helpers.test.ts. */
179
+ const qualifiedName = routine.schema
180
+ ? `${this.platform.quoteIdentifier(routine.schema.toUpperCase())}.${quotedName}`
181
+ : quotedName;
182
+ const oracleConn = await this.acquireOracleConnection();
183
+ try {
184
+ if (routine.type === 'function') {
185
+ const argList = routine.params.map(p => `:${p.name}`).join(', ');
186
+ const block = `BEGIN :mo_ret := ${qualifiedName}(${argList}); END;`;
187
+ const bindings = { mo_ret: oracleReturnBind(this.platform, routine) };
188
+ for (const p of routine.params) {
189
+ bindings[p.name] = {
190
+ dir: oracledb.BIND_IN,
191
+ val: this.convertRoutineInbound(args[p.name], p),
192
+ };
193
+ }
194
+ const result = await oracleConn.execute(block, bindings, { autoCommit: true });
195
+ return this.convertRoutineOutbound(result.outBinds?.mo_ret, routine.returnCustomType);
196
+ }
197
+ const argList = routine.params.map(p => `:${p.name}`).join(', ');
198
+ const block = `BEGIN ${qualifiedName}(${argList}); END;`;
199
+ const bindings = {};
200
+ const refCursorParams = [];
201
+ for (const p of routine.params) {
202
+ const value = this.convertRoutineInbound(args[p.name], p);
203
+ const isRefCursor = typeof p.type === 'string' && /sys_refcursor|ref\s*cursor/i.test(p.type);
204
+ if (p.direction === 'in') {
205
+ bindings[p.name] = { dir: oracledb.BIND_IN, val: value };
206
+ continue;
207
+ }
208
+ if (isRefCursor) {
209
+ bindings[p.name] = { dir: oracledb.BIND_OUT, type: oracledb.DB_TYPE_CURSOR };
210
+ refCursorParams.push(p.name);
211
+ continue;
212
+ }
213
+ const binding = {
214
+ dir: p.direction === 'out' ? oracledb.BIND_OUT : oracledb.BIND_INOUT,
215
+ ...oracleBindTypeFromRuntime(this.platform, p.runtimeType),
216
+ };
217
+ if (p.direction === 'inout') {
218
+ binding.val = value;
219
+ }
220
+ bindings[p.name] = binding;
221
+ }
222
+ const result = await oracleConn.execute(block, bindings, {
223
+ autoCommit: true,
224
+ outFormat: oracledb.OUT_FORMAT_OBJECT,
225
+ });
226
+ const outBinds = result.outBinds;
227
+ if (refCursorParams.length > 0 && outBinds) {
228
+ const sets = [];
229
+ for (const name of refCursorParams) {
230
+ const cursor = outBinds[name];
231
+ const rows = await cursor.getRows(0);
232
+ await cursor.close();
233
+ // outBinds ResultSets use raw column metadata (uppercase); lowercase for cross-driver parity.
234
+ sets.push(rows.map(row => Object.fromEntries(Object.entries(row).map(([k, v]) => [k.toLowerCase(), v]))));
235
+ }
236
+ return sets;
237
+ }
238
+ if (outBinds) {
239
+ this.applyRoutineOutParams(outBinds, routine.params.filter(p => p.direction !== 'in'), args);
240
+ }
241
+ return undefined;
242
+ }
243
+ finally {
244
+ await oracleConn.close();
245
+ }
246
+ }
136
247
  stripTrailingSemicolon(sql) {
137
248
  if (sql.endsWith(';') && !/end(\s+\w+)?;$/i.test(sql)) {
138
249
  return sql.slice(0, -1);
package/OraclePlatform.js CHANGED
@@ -3,6 +3,8 @@ import oracledb from 'oracledb';
3
3
  import { OracleSchemaHelper } from './OracleSchemaHelper.js';
4
4
  import { OracleExceptionConverter } from './OracleExceptionConverter.js';
5
5
  import { OracleSchemaGenerator } from './OracleSchemaGenerator.js';
6
+ // `bigint` deliberately absent — falls through to DB_TYPE_VARCHAR so oracledb returns a
7
+ // string and the caller doesn't lose precision on values past Number.MAX_SAFE_INTEGER.
6
8
  const ORACLE_TYPE_MAP = {
7
9
  string: oracledb.DB_TYPE_VARCHAR,
8
10
  number: oracledb.DB_TYPE_NUMBER,
@@ -48,6 +48,10 @@ export class OracleSchemaGenerator extends SchemaGenerator {
48
48
  await this.ensureDatabase();
49
49
  const metadata = this.getOrderedMetadata(options.schema).reverse();
50
50
  const ret = [];
51
+ // Routines before tables — bodies reference columns and Oracle has no CASCADE for them.
52
+ for (const routine of this.getTargetSchema(options.schema).getRoutines()) {
53
+ this.helper.append(ret, this.helper.dropRoutine(routine), true);
54
+ }
51
55
  for (const meta of metadata) {
52
56
  const schemaName = options.schema ?? this.config.get('schema');
53
57
  /* v8 ignore next: wildcard schema branch */
@@ -1,4 +1,4 @@
1
- import { type AbstractSqlConnection, type CheckDef, type Column, type DatabaseSchema, type DatabaseTable, type Dictionary, type ForeignKey, type IndexDef, SchemaHelper, type Table, type TableDifference, type Transaction, type Type } from '@mikro-orm/sql';
1
+ import { type AbstractSqlConnection, type CheckDef, type Column, type DatabaseSchema, type DatabaseTable, type Dictionary, type ForeignKey, type IndexDef, SchemaHelper, type Table, type TableDifference, type Transaction, type Type, type SqlRoutineDef } from '@mikro-orm/sql';
2
2
  /** Schema introspection helper for Oracle Database. */
3
3
  export declare class OracleSchemaHelper extends SchemaHelper {
4
4
  static readonly DEFAULT_VALUES: Record<string, string[]>;
@@ -53,4 +53,12 @@ export declare class OracleSchemaHelper extends SchemaHelper {
53
53
  appendComments(table: DatabaseTable): string[];
54
54
  inferLengthFromColumnType(type: string): number | undefined;
55
55
  protected wrap(val: string | undefined, type: Type<unknown>): string | undefined;
56
+ /** Oracle identifiers are upper-cased before quoting to match `USER_PROCEDURES` introspection. Cross-schema qualifier mirrors `OracleConnection.callRoutine`. */
57
+ private qualifiedOracleRoutineName;
58
+ createRoutine(routine: SqlRoutineDef): string;
59
+ /** Uses `IF EXISTS` (Oracle 23c+); older versions can extend this helper. */
60
+ dropRoutine(routine: SqlRoutineDef): string;
61
+ getAllRoutines(connection: AbstractSqlConnection): Promise<SqlRoutineDef[]>;
62
+ private getAllRoutineParams;
63
+ private unwrapPlSqlBody;
56
64
  }
@@ -1,4 +1,7 @@
1
1
  import { DateTimeType, EnumType, SchemaHelper, StringType, TextType, Utils, } from '@mikro-orm/sql';
2
+ function stripTypeLength(type) {
3
+ return type.replace(/\(\s*[^)]*\)\s*$/, '').trim();
4
+ }
2
5
  /** Schema introspection helper for Oracle Database. */
3
6
  export class OracleSchemaHelper extends SchemaHelper {
4
7
  static DEFAULT_VALUES = {
@@ -664,4 +667,107 @@ export class OracleSchemaHelper extends SchemaHelper {
664
667
  const stringType = type instanceof StringType || type instanceof TextType || type instanceof EnumType;
665
668
  return typeof val === 'string' && val.length > 0 && stringType ? this.platform.quoteValue(val) : val;
666
669
  }
670
+ /** Oracle identifiers are upper-cased before quoting to match `USER_PROCEDURES` introspection. Cross-schema qualifier mirrors `OracleConnection.callRoutine`. */
671
+ qualifiedOracleRoutineName(routine) {
672
+ const name = this.quote(routine.name.toUpperCase());
673
+ const defaultSchema = this.platform.getDefaultSchemaName();
674
+ if (routine.schema && routine.schema !== defaultSchema) {
675
+ return `${this.quote(routine.schema.toUpperCase())}.${name}`;
676
+ }
677
+ return name;
678
+ }
679
+ createRoutine(routine) {
680
+ if (routine.expression) {
681
+ return routine.expression;
682
+ }
683
+ const name = this.qualifiedOracleRoutineName(routine);
684
+ const params = routine.params
685
+ .map(p => {
686
+ const dir = p.direction === 'in' ? 'IN' : p.direction === 'out' ? 'OUT' : 'IN OUT';
687
+ // PL/SQL formal params require unconstrained types — `VARCHAR2`, not `VARCHAR2(255)`.
688
+ return `${this.quote(p.name.toUpperCase())} ${dir} ${stripTypeLength(p.type)}`;
689
+ })
690
+ .join(', ');
691
+ const argsClause = routine.params.length ? `(${params})` : '';
692
+ const body = this.wrapRoutineBody(routine.body ?? '');
693
+ if (routine.type === 'procedure') {
694
+ return `create or replace procedure ${name}${argsClause} as ${body};`;
695
+ }
696
+ const returnType = stripTypeLength(routine.returns?.type ?? 'VARCHAR2');
697
+ return `create or replace function ${name}${argsClause} return ${returnType} as ${body};`;
698
+ }
699
+ /** Uses `IF EXISTS` (Oracle 23c+); older versions can extend this helper. */
700
+ dropRoutine(routine) {
701
+ const kind = routine.type === 'procedure' ? 'procedure' : 'function';
702
+ return `drop ${kind} if exists ${this.qualifiedOracleRoutineName(routine)}`;
703
+ }
704
+ async getAllRoutines(connection) {
705
+ const sql = `
706
+ select
707
+ p.OBJECT_NAME as name,
708
+ p.OBJECT_TYPE as kind,
709
+ (
710
+ select listagg(s.TEXT, '') within group (order by s.LINE)
711
+ from USER_SOURCE s
712
+ where s.NAME = p.OBJECT_NAME and s.TYPE = p.OBJECT_TYPE
713
+ ) as source
714
+ from USER_PROCEDURES p
715
+ where p.OBJECT_TYPE in ('PROCEDURE', 'FUNCTION')
716
+ and p.PROCEDURE_NAME is null
717
+ `;
718
+ const [rows, paramsAndReturns] = await Promise.all([
719
+ connection.execute(sql),
720
+ this.getAllRoutineParams(connection),
721
+ ]);
722
+ const { params, returns } = paramsAndReturns;
723
+ // Surface the connected user as the schema so the comparator's routineKey matches the
724
+ // metadata side (which fills `schema` from `getDefaultSchemaName()` when not declared).
725
+ const schemaName = this.platform.getDefaultSchemaName();
726
+ return rows.map(row => ({
727
+ name: row.name,
728
+ schema: schemaName,
729
+ type: row.kind.toLowerCase(),
730
+ body: this.unwrapPlSqlBody(row.source ?? ''),
731
+ params: params.get(row.name) ?? [],
732
+ returns: row.kind === 'FUNCTION' ? (returns.get(row.name) ?? { type: 'VARCHAR2', nullable: true }) : undefined,
733
+ }));
734
+ }
735
+ async getAllRoutineParams(connection) {
736
+ // POSITION = 0 / ARGUMENT_NAME = NULL is the function return type; `PACKAGE_NAME is null`
737
+ // restricts to standalone routines so packaged ones don't leak through name collisions.
738
+ const sql = `
739
+ select
740
+ OBJECT_NAME as routine_name,
741
+ ARGUMENT_NAME as param_name,
742
+ DATA_TYPE as type,
743
+ IN_OUT as direction,
744
+ POSITION as position
745
+ from USER_ARGUMENTS
746
+ where PACKAGE_NAME is null
747
+ order by OBJECT_NAME, POSITION
748
+ `;
749
+ const rows = await connection.execute(sql);
750
+ const params = new Map();
751
+ const returns = new Map();
752
+ for (const row of rows) {
753
+ if (row.position === 0 && row.param_name == null) {
754
+ returns.set(row.routine_name, { type: row.type, nullable: true });
755
+ continue;
756
+ }
757
+ if (!params.has(row.routine_name)) {
758
+ params.set(row.routine_name, []);
759
+ }
760
+ const direction = row.direction === 'IN/OUT' ? 'inout' : row.direction === 'OUT' ? 'out' : 'in';
761
+ params.get(row.routine_name).push({
762
+ name: row.param_name,
763
+ type: row.type,
764
+ direction,
765
+ });
766
+ }
767
+ return { params, returns };
768
+ }
769
+ unwrapPlSqlBody(source) {
770
+ const match = /\b(?:as|is)\s+(?:begin\s+)?([\s\S]*?)\s*end\s*;?\s*$/i.exec(source.trim());
771
+ return match ? match[1].trim() : source.trim();
772
+ }
667
773
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/oracledb",
3
- "version": "7.1.0-dev.43",
3
+ "version": "7.1.0-dev.44",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL, SQLite, MSSQL and Oracle databases.",
5
5
  "keywords": [
6
6
  "data-mapper",
@@ -42,7 +42,7 @@
42
42
  "copy": "node ../../scripts/copy.mjs"
43
43
  },
44
44
  "dependencies": {
45
- "@mikro-orm/sql": "7.1.0-dev.43",
45
+ "@mikro-orm/sql": "7.1.0-dev.44",
46
46
  "kysely": "0.29.2",
47
47
  "oracledb": "6.10.0"
48
48
  },
@@ -50,7 +50,7 @@
50
50
  "@mikro-orm/core": "^7.0.17"
51
51
  },
52
52
  "peerDependencies": {
53
- "@mikro-orm/core": "7.1.0-dev.43"
53
+ "@mikro-orm/core": "7.1.0-dev.44"
54
54
  },
55
55
  "engines": {
56
56
  "node": ">= 22.17.0"