@mikro-orm/oracledb 7.1.0-dev.9 → 7.1.1-dev.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.
@@ -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);
@@ -1,5 +1,5 @@
1
- import { MikroORM, type Options, type IDatabaseDriver, type EntityManager, type EntityManagerType, type EntityClass, type AnyEntity, type EntitySchema } from '@mikro-orm/core';
2
- import type { SqlEntityManager } from '@mikro-orm/sql';
1
+ import { type MikroORM, type Options, type IDatabaseDriver, type EntityManager, type EntityManagerType, type EntityClass, type AnyEntity, type EntitySchema } from '@mikro-orm/core';
2
+ import { SqlMikroORM, type SqlEntityManager } from '@mikro-orm/sql';
3
3
  import { OracleDriver } from './OracleDriver.js';
4
4
  /** Configuration options for the Oracle driver. */
5
5
  export type OracleOptions<EM extends SqlEntityManager<OracleDriver> = SqlEntityManager<OracleDriver>, Entities extends readonly (string | EntityClass<AnyEntity> | EntitySchema)[] = (string | EntityClass<AnyEntity> | EntitySchema)[]> = Partial<Options<OracleDriver, EM, Entities>>;
@@ -8,7 +8,7 @@ export declare function defineOracleConfig<EM extends SqlEntityManager<OracleDri
8
8
  /**
9
9
  * @inheritDoc
10
10
  */
11
- export declare class OracleMikroORM<EM extends SqlEntityManager<OracleDriver> = SqlEntityManager<OracleDriver>, Entities extends readonly (string | EntityClass<AnyEntity> | EntitySchema)[] = (string | EntityClass<AnyEntity> | EntitySchema)[]> extends MikroORM<OracleDriver, EM, Entities> {
11
+ export declare class OracleMikroORM<EM extends SqlEntityManager<OracleDriver> = SqlEntityManager<OracleDriver>, Entities extends readonly (string | EntityClass<AnyEntity> | EntitySchema)[] = (string | EntityClass<AnyEntity> | EntitySchema)[]> extends SqlMikroORM<OracleDriver, EM, Entities> {
12
12
  /**
13
13
  * @inheritDoc
14
14
  */
package/OracleMikroORM.js CHANGED
@@ -1,4 +1,5 @@
1
- import { defineConfig, MikroORM, } from '@mikro-orm/core';
1
+ import { defineConfig, } from '@mikro-orm/core';
2
+ import { SqlMikroORM } from '@mikro-orm/sql';
2
3
  import { OracleDriver } from './OracleDriver.js';
3
4
  /** Creates a type-safe configuration object for the Oracle driver. */
4
5
  export function defineOracleConfig(options) {
@@ -7,7 +8,7 @@ export function defineOracleConfig(options) {
7
8
  /**
8
9
  * @inheritDoc
9
10
  */
10
- export class OracleMikroORM extends MikroORM {
11
+ export class OracleMikroORM extends SqlMikroORM {
11
12
  /**
12
13
  * @inheritDoc
13
14
  */
@@ -16,6 +16,8 @@ export declare class OraclePlatform extends AbstractSqlPlatform {
16
16
  readOnly?: boolean;
17
17
  }): string[];
18
18
  usesAsKeyword(): boolean;
19
+ /** Oracle 12.2+ identifier limit (pre-12.2 is 30 chars). */
20
+ getMaxIdentifierLength(): number;
19
21
  compareUuids(): string;
20
22
  convertUuidToJSValue(value: Buffer): string;
21
23
  convertUuidToDatabaseValue(value: string): Buffer;
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 +50,10 @@ export class OraclePlatform extends AbstractSqlPlatform {
48
50
  usesAsKeyword() {
49
51
  return false;
50
52
  }
53
+ /** Oracle 12.2+ identifier limit (pre-12.2 is 30 chars). */
54
+ getMaxIdentifierLength() {
55
+ return 128;
56
+ }
51
57
  compareUuids() {
52
58
  return 'any';
53
59
  }
@@ -203,7 +209,7 @@ export class OraclePlatform extends AbstractSqlPlatform {
203
209
  if (b.length === 0) {
204
210
  return raw(`json_equal(${root}, json(?))`, [value]);
205
211
  }
206
- return raw(`json_value(${root}, '$.${b.map(this.quoteJsonKey).join('.')}')`);
212
+ return raw(`json_value(${root}, ${this.quoteValue(`$.${b.map(this.quoteJsonKey).join('.')}`)})`);
207
213
  }
208
214
  processJsonCondition(o, value, path, alias) {
209
215
  if (Utils.isPlainObject(value) && !Object.keys(value).some(k => Utils.isOperator(k))) {
@@ -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,8 +1,14 @@
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[]>;
5
+ private static readonly AUTO_NOT_NULL_RE;
6
+ private static readonly PARTIAL_INDEX_RE;
5
7
  getDatabaseExistsSQL(name: string): string;
8
+ getSetSchemaSQL(schema: string): string;
9
+ getResetSchemaSQL(defaultSchema: string): string;
10
+ supportsMigrationSchema(): boolean;
11
+ tableExists(connection: AbstractSqlConnection, tableName: string, schemaName: string | undefined, ctx?: Transaction): Promise<boolean>;
6
12
  getAllTables(connection: AbstractSqlConnection, schemas?: string[], ctx?: Transaction): Promise<Table[]>;
7
13
  getListTablesSQL(schemaName?: string): string;
8
14
  getListViewsSQL(): string;
@@ -19,6 +25,7 @@ export declare class OracleSchemaHelper extends SchemaHelper {
19
25
  private getEnumDefinitions;
20
26
  private getChecksSQL;
21
27
  getAllChecks(connection: AbstractSqlConnection, tablesBySchemas: Map<string | undefined, Table[]>, ctx?: Transaction): Promise<Dictionary<CheckDef[]>>;
28
+ getDatabaseCollation(connection: AbstractSqlConnection, ctx?: Transaction): Promise<string | undefined>;
22
29
  loadInformationSchema(schema: DatabaseSchema, connection: AbstractSqlConnection, tables: Table[], schemas?: string[], ctx?: Transaction): Promise<void>;
23
30
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
24
31
  getPostAlterTable(tableDiff: TableDifference, safe: boolean): string[];
@@ -35,6 +42,9 @@ export declare class OracleSchemaHelper extends SchemaHelper {
35
42
  createTableColumn(column: Column, table: DatabaseTable, changedProperties?: Set<string>): string | undefined;
36
43
  alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
37
44
  getCreateIndexSQL(tableName: string, index: IndexDef, partialExpression?: boolean): string;
45
+ protected getIndexColumns(index: IndexDef): string;
46
+ /** Oracle has no native WHERE clause for indexes; the predicate is folded into CASE-WHEN columns. */
47
+ protected getIndexWhereClause(_index: IndexDef): string;
38
48
  createIndex(index: IndexDef, table: DatabaseTable, createPrimary?: boolean): string;
39
49
  dropForeignKey(tableName: string, constraintName: string): string;
40
50
  dropViewIfExists(name: string, schema?: string): string;
@@ -43,4 +53,12 @@ export declare class OracleSchemaHelper extends SchemaHelper {
43
53
  appendComments(table: DatabaseTable): string[];
44
54
  inferLengthFromColumnType(type: string): number | undefined;
45
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;
46
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 = {
@@ -7,9 +10,29 @@ export class OracleSchemaHelper extends SchemaHelper {
7
10
  systimestamp: ['current_timestamp'],
8
11
  sysdate: ['current_timestamp'],
9
12
  };
13
+ // `stripAutoNotNullFilter` unwraps balanced per-clause parens before calling `.exec`, so we
14
+ // only need to match the bare form here.
15
+ static AUTO_NOT_NULL_RE = /^"?([^"\s()]+)"?\s+is\s+not\s+null$/i;
16
+ // Greedy `(.+)` so nested CASE expressions inside the predicate don't trip the match on
17
+ // an inner `then <col> end`.
18
+ static PARTIAL_INDEX_RE = /^\s*\(?\s*case\s+when\s+(.+)\s+then\s+"?([^"\s)]+)"?\s+end\s*\)?\s*$/is;
10
19
  getDatabaseExistsSQL(name) {
11
20
  return `select 1 from all_users where username = ${this.platform.quoteValue(name)}`;
12
21
  }
22
+ getSetSchemaSQL(schema) {
23
+ return `alter session set current_schema = ${this.quote(schema)}`;
24
+ }
25
+ getResetSchemaSQL(defaultSchema) {
26
+ return `alter session set current_schema = ${this.quote(defaultSchema)}`;
27
+ }
28
+ supportsMigrationSchema() {
29
+ return true;
30
+ }
31
+ async tableExists(connection, tableName, schemaName, ctx) {
32
+ const schema = schemaName ?? this.platform.getDefaultSchemaName();
33
+ const rows = await connection.execute(`select 1 from all_tables where owner = ${this.platform.quoteValue(schema)} and table_name = ${this.platform.quoteValue(tableName)}`, [], 'all', ctx);
34
+ return rows.length > 0;
35
+ }
13
36
  async getAllTables(connection, schemas, ctx) {
14
37
  if (!schemas || schemas.length === 0) {
15
38
  return connection.execute(this.getListTablesSQL(), [], 'all', ctx);
@@ -121,7 +144,8 @@ export class OracleSchemaHelper extends SchemaHelper {
121
144
  atc.char_length as character_maximum_length,
122
145
  atc.data_length as data_length,
123
146
  atc.identity_column as is_identity,
124
- atc.column_id as ordinal_position
147
+ atc.column_id as ordinal_position,
148
+ atc.collation as collation_name
125
149
  from all_tab_cols atc
126
150
  left join all_col_comments acc on atc.owner = acc.owner and atc.table_name = acc.table_name and atc.column_name = acc.column_name
127
151
  where atc.hidden_column = 'NO'
@@ -177,6 +201,11 @@ export class OracleSchemaHelper extends SchemaHelper {
177
201
  precision: col.numeric_precision,
178
202
  scale: col.numeric_scale,
179
203
  comment: col.column_comment,
204
+ // `USING_NLS_COMP` is the Oracle sentinel for "inherit from the session NLS settings", so
205
+ // a column without an explicit `COLLATE` clause introspects as `USING_NLS_COMP` rather
206
+ // than NULL. Treat it as "no explicit collation" so the comparator doesn't flag every
207
+ // string column as changed when the database default is something else (e.g. `BINARY`).
208
+ collation: col.collation_name && col.collation_name !== 'USING_NLS_COMP' ? col.collation_name : undefined,
180
209
  generated,
181
210
  });
182
211
  }
@@ -210,16 +239,19 @@ export class OracleSchemaHelper extends SchemaHelper {
210
239
  if (isPrimary) {
211
240
  continue;
212
241
  }
242
+ const partialMatch = typeof index.expression === 'string' ? OracleSchemaHelper.PARTIAL_INDEX_RE.exec(index.expression) : null;
213
243
  const indexDef = {
214
- columnNames: [index.column_name],
244
+ columnNames: [partialMatch ? partialMatch[2] : index.column_name],
215
245
  keyName: index.index_name,
216
246
  unique: index.is_unique === 'YES',
217
- primary: false, // We skip PK indexes above, so this is always false
247
+ primary: false,
218
248
  constraint: isConstraintIndex || index.is_unique === 'YES',
219
249
  };
220
- // Handle function-based indexes (expression indexes)
221
- /* v8 ignore start: expression index branches */
222
- if (index.expression) {
250
+ /* v8 ignore start: function-based (non-partial) index branches */
251
+ if (partialMatch) {
252
+ indexDef.where = partialMatch[1].trim();
253
+ }
254
+ else if (index.expression) {
223
255
  indexDef.expression = index.expression;
224
256
  }
225
257
  else if (index.column_name?.match(/[(): ,"'`]/)) {
@@ -231,6 +263,17 @@ export class OracleSchemaHelper extends SchemaHelper {
231
263
  }
232
264
  for (const key of Object.keys(ret)) {
233
265
  ret[key] = await this.mapIndexes(ret[key]);
266
+ for (const idx of ret[key]) {
267
+ if (idx.where) {
268
+ const stripped = this.stripAutoNotNullFilter(idx.where, idx.columnNames, OracleSchemaHelper.AUTO_NOT_NULL_RE);
269
+ if (stripped === '') {
270
+ delete idx.where;
271
+ }
272
+ else {
273
+ idx.where = stripped;
274
+ }
275
+ }
276
+ }
234
277
  }
235
278
  return ret;
236
279
  }
@@ -338,6 +381,10 @@ export class OracleSchemaHelper extends SchemaHelper {
338
381
  }
339
382
  return ret;
340
383
  }
384
+ async getDatabaseCollation(connection, ctx) {
385
+ const [row] = await connection.execute(`select property_value as collation from database_properties where property_name = 'DEFAULT_COLLATION'`, [], 'all', ctx);
386
+ return row?.collation;
387
+ }
341
388
  async loadInformationSchema(schema, connection, tables, schemas, ctx) {
342
389
  if (tables.length === 0) {
343
390
  return;
@@ -347,9 +394,11 @@ export class OracleSchemaHelper extends SchemaHelper {
347
394
  const indexes = await this.getAllIndexes(connection, tablesBySchema, ctx);
348
395
  const checks = await this.getAllChecks(connection, tablesBySchema, ctx);
349
396
  const fks = await this.getAllForeignKeys(connection, tablesBySchema, ctx);
397
+ const dbCollation = await this.getDatabaseCollation(connection, ctx);
350
398
  for (const t of tables) {
351
399
  const key = this.getTableKey(t);
352
400
  const table = schema.addTable(t.table_name, t.schema_name, t.table_comment);
401
+ table.collation = dbCollation;
353
402
  const pks = await this.getPrimaryKeys(connection, indexes[key], table.name, table.schema);
354
403
  const enums = this.getEnumDefinitions(checks[key] ?? []);
355
404
  table.init(columns[key], indexes[key], checks[key], pks, fks[key], enums);
@@ -450,6 +499,7 @@ export class OracleSchemaHelper extends SchemaHelper {
450
499
  /* v8 ignore next: generated column branch */
451
500
  const columnType = column.generated ? `as ${column.generated}` : column.type;
452
501
  const col = [this.quote(column.name), columnType];
502
+ Utils.runIfNotEmpty(() => col.push(this.getCollateSQL(column.collation)), column.collation);
453
503
  Utils.runIfNotEmpty(() => col.push('generated by default as identity'), column.autoincrement);
454
504
  /* v8 ignore next 3: default value branch */
455
505
  const useDefault = changedProperties
@@ -473,8 +523,15 @@ export class OracleSchemaHelper extends SchemaHelper {
473
523
  const parts = [];
474
524
  const quotedTableName = table.getQuotedName();
475
525
  // Oracle uses MODIFY for column changes, and always requires the column type
476
- if (changedProperties.has('type') || changedProperties.has('nullable') || changedProperties.has('default')) {
526
+ if (changedProperties.has('type') ||
527
+ changedProperties.has('nullable') ||
528
+ changedProperties.has('default') ||
529
+ changedProperties.has('collation')) {
477
530
  const colParts = [this.quote(column.name), column.type];
531
+ // Oracle's MODIFY restates the column type, so re-emit COLLATE whenever the column has
532
+ // one — even if the trigger was a nullable/default change — so we can't accidentally
533
+ // drop the column collation by omitting it from the new type spec.
534
+ Utils.runIfNotEmpty(() => colParts.push(this.getCollateSQL(column.collation)), column.collation);
478
535
  if (changedProperties.has('default')) {
479
536
  if (column.default != null && column.default !== 'null') {
480
537
  colParts.push(`default ${column.default}`);
@@ -503,6 +560,16 @@ export class OracleSchemaHelper extends SchemaHelper {
503
560
  }
504
561
  return super.getCreateIndexSQL(tableName, index);
505
562
  }
563
+ getIndexColumns(index) {
564
+ if (index.where) {
565
+ return this.emulatePartialIndexColumns(index);
566
+ }
567
+ return super.getIndexColumns(index);
568
+ }
569
+ /** Oracle has no native WHERE clause for indexes; the predicate is folded into CASE-WHEN columns. */
570
+ getIndexWhereClause(_index) {
571
+ return '';
572
+ }
506
573
  createIndex(index, table, createPrimary = false) {
507
574
  if (index.primary) {
508
575
  return '';
@@ -524,10 +591,21 @@ export class OracleSchemaHelper extends SchemaHelper {
524
591
  const quotedTableName = table.getQuotedName();
525
592
  if (index.unique) {
526
593
  const nullableCols = index.columnNames.filter(column => table.getColumn(column)?.nullable);
594
+ const autoNotNull = nullableCols.length
595
+ ? nullableCols.map(c => `${this.quote(c)} is not null`).join(' and ')
596
+ : '';
597
+ if (index.where) {
598
+ // Wrap the user predicate in parens before ANDing the auto-NOT-NULL guard — otherwise a
599
+ // disjunctive `a = 1 or b = 2` would rebind as `a = 1 or (b = 2 and <col> is not null)`.
600
+ const predicate = autoNotNull ? `(${index.where}) and ${autoNotNull}` : index.where;
601
+ return `create unique index ${this.quote(index.keyName)} on ${quotedTableName} (${index.columnNames
602
+ .map(c => `case when ${predicate} then ${this.quote(c)} end`)
603
+ .join(', ')})`;
604
+ }
527
605
  return `create unique index ${this.quote(index.keyName)} on ${quotedTableName} (${index.columnNames
528
606
  .map(c => {
529
607
  if (table.getColumn(c)?.nullable) {
530
- return `case when ${nullableCols.map(c => `${this.quote(c)} is not null`).join(' and ')} then ${this.quote(c)} end`;
608
+ return `case when ${autoNotNull} then ${this.quote(c)} end`;
531
609
  }
532
610
  return this.quote(c);
533
611
  })
@@ -589,4 +667,107 @@ export class OracleSchemaHelper extends SchemaHelper {
589
667
  const stringType = type instanceof StringType || type instanceof TextType || type instanceof EnumType;
590
668
  return typeof val === 'string' && val.length > 0 && stringType ? this.platform.quoteValue(val) : val;
591
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
+ }
592
773
  }
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
  <a href="https://mikro-orm.io"><img src="https://raw.githubusercontent.com/mikro-orm/mikro-orm/master/docs/static/img/logo-readme.svg?sanitize=true" alt="MikroORM" /></a>
3
3
  </h1>
4
4
 
5
- TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL, SQLite (including libSQL), MSSQL and Oracle databases.
5
+ TypeScript ORM for Node.js based on Data Mapper, [Unit of Work](https://mikro-orm.io/docs/unit-of-work/) and [Identity Map](https://mikro-orm.io/docs/identity-map/) patterns. Supports MongoDB, MySQL, MariaDB, PostgreSQL (including CockroachDB and PGlite), SQLite (including libSQL), MSSQL and Oracle databases.
6
6
 
7
7
  > Heavily inspired by [Doctrine](https://www.doctrine-project.org/) and [Hibernate](https://hibernate.org/).
8
8
 
@@ -19,6 +19,7 @@ Install a driver package for your database:
19
19
 
20
20
  ```sh
21
21
  npm install @mikro-orm/postgresql # PostgreSQL
22
+ npm install @mikro-orm/pglite # PGlite (embedded PostgreSQL in WASM)
22
23
  npm install @mikro-orm/mysql # MySQL
23
24
  npm install @mikro-orm/mariadb # MariaDB
24
25
  npm install @mikro-orm/sqlite # SQLite
package/index.d.ts CHANGED
@@ -8,3 +8,7 @@ export * from './OracleSchemaGenerator.js';
8
8
  export * from './OracleExceptionConverter.js';
9
9
  export type { OracleOptions as Options } from './OracleMikroORM.js';
10
10
  export { OracleMikroORM as MikroORM, defineOracleConfig as defineConfig } from './OracleMikroORM.js';
11
+ import { type AbstractSqlDriver, SqlEntityManager } from '@mikro-orm/sql';
12
+ import type { OracleDriver } from './OracleDriver.js';
13
+ export type EntityManager<Driver extends AbstractSqlDriver = OracleDriver> = SqlEntityManager<Driver>;
14
+ export declare const EntityManager: typeof SqlEntityManager;
package/index.js CHANGED
@@ -7,3 +7,5 @@ export * from './OracleSchemaHelper.js';
7
7
  export * from './OracleSchemaGenerator.js';
8
8
  export * from './OracleExceptionConverter.js';
9
9
  export { OracleMikroORM as MikroORM, defineOracleConfig as defineConfig } from './OracleMikroORM.js';
10
+ import { SqlEntityManager } from '@mikro-orm/sql';
11
+ export const EntityManager = SqlEntityManager;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/oracledb",
3
- "version": "7.1.0-dev.9",
3
+ "version": "7.1.1-dev.0",
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,15 +42,15 @@
42
42
  "copy": "node ../../scripts/copy.mjs"
43
43
  },
44
44
  "dependencies": {
45
- "@mikro-orm/sql": "7.1.0-dev.9",
46
- "kysely": "0.28.16",
45
+ "@mikro-orm/sql": "7.1.1-dev.0",
46
+ "kysely": "0.29.2",
47
47
  "oracledb": "6.10.0"
48
48
  },
49
49
  "devDependencies": {
50
- "@mikro-orm/core": "^7.0.11"
50
+ "@mikro-orm/core": "^7.1.0"
51
51
  },
52
52
  "peerDependencies": {
53
- "@mikro-orm/core": "7.1.0-dev.9"
53
+ "@mikro-orm/core": "7.1.1-dev.0"
54
54
  },
55
55
  "engines": {
56
56
  "node": ">= 22.17.0"