* Oracle routines acquire their own pool connection with `autoCommit: true` (refcursor binds * and DML need to resolve in one round-trip), so they cannot share the EM's transaction. Wrapping * a writing routine in `em.transactional(...)` would silently auto-commit its writes. Read-only * fu
(routine: Routine, args: Record<string, unknown> = {}, ctx?: Transaction)
| 227 | * functions are allowed through. |
| 228 | */ |
| 229 | override async callRoutine<T>(routine: Routine, args: Record<string, unknown> = {}, ctx?: Transaction): Promise<T> { |
| 230 | /* v8 ignore next 3 */ |
| 231 | if (!this.oraclePool || !this.acquireOracleConnection) { |
| 232 | throw new Error('Oracle pool not initialised — call connect() before callRoutine().'); |
| 233 | } |
| 234 | |
| 235 | if (ctx && !isReadOnlyRoutine(routine)) { |
| 236 | throw new Error( |
| 237 | `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.`, |
| 238 | ); |
| 239 | } |
| 240 | |
| 241 | const name = routine.name.toUpperCase(); |
| 242 | // Cross-schema needs an `OWNER.NAME` prefix; mirror the schema helper's quoting on both branches. |
| 243 | const quotedName = this.platform.quoteIdentifier(name); |
| 244 | /* v8 ignore next 3 — cross-schema invocation isn't reachable from the single-user test DB; |
| 245 | the qualified DDL path (createRoutine/dropRoutine) is covered by helpers.test.ts. */ |
| 246 | const qualifiedName = routine.schema |
| 247 | ? `${this.platform.quoteIdentifier(routine.schema.toUpperCase())}.${quotedName}` |
| 248 | : quotedName; |
| 249 | const oracleConn = await this.acquireOracleConnection(); |
| 250 | |
| 251 | try { |
| 252 | if (routine.type === 'function') { |
| 253 | const argList = routine.params.map(p => `:${p.name}`).join(', '); |
| 254 | const block = `BEGIN :mo_ret := ${qualifiedName}(${argList}); END;`; |
| 255 | const bindings: Dictionary = { mo_ret: oracleReturnBind(this.platform as OraclePlatform, routine) }; |
| 256 | |
| 257 | for (const p of routine.params) { |
| 258 | bindings[p.name as string] = { |
| 259 | dir: oracledb.BIND_IN, |
| 260 | val: this.convertRoutineInbound(args[p.name as string], p), |
| 261 | }; |
| 262 | } |
| 263 | |
| 264 | const result = await oracleConn.execute(block, bindings, { autoCommit: true }); |
| 265 | return this.convertRoutineOutbound<T>((result.outBinds as Dictionary)?.mo_ret, routine.returnCustomType); |
| 266 | } |
| 267 | |
| 268 | const argList = routine.params.map(p => `:${p.name}`).join(', '); |
| 269 | const block = `BEGIN ${qualifiedName}(${argList}); END;`; |
| 270 | const bindings: Dictionary = {}; |
| 271 | const refCursorParams: string[] = []; |
| 272 | |
| 273 | for (const p of routine.params) { |
| 274 | const value = this.convertRoutineInbound(args[p.name as string], p); |
| 275 | const isRefCursor = typeof p.type === 'string' && /sys_refcursor|ref\s*cursor/i.test(p.type); |
| 276 | |
| 277 | if (p.direction === 'in') { |
| 278 | bindings[p.name as string] = { dir: oracledb.BIND_IN, val: value }; |
| 279 | continue; |
| 280 | } |
| 281 | |
| 282 | if (isRefCursor) { |
| 283 | bindings[p.name as string] = { dir: oracledb.BIND_OUT, type: oracledb.DB_TYPE_CURSOR }; |
| 284 | refCursorParams.push(p.name as string); |
| 285 | continue; |
| 286 | } |
nothing calls this directly
no test coverage detected