@mikro-orm/oracledb 7.0.7 → 7.0.8-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,417 +1,399 @@
1
- import { SchemaGenerator, SchemaComparator, DatabaseSchema } from '@mikro-orm/sql';
1
+ import { SchemaGenerator, SchemaComparator, DatabaseSchema, } from '@mikro-orm/sql';
2
2
  /** Schema generator with Oracle-specific behavior for multi-schema support and privilege management. */
3
3
  export class OracleSchemaGenerator extends SchemaGenerator {
4
- /** Tracks whether the main user has been granted DBA (or equivalent) privileges. */
5
- hasDbaGrant = false;
6
- static register(orm) {
7
- orm.config.registerExtension('@mikro-orm/schema-generator', () => new OracleSchemaGenerator(orm.em));
8
- }
9
- /**
10
- * creates new database and connects to it
11
- */
12
- async createDatabase(name) {
13
- name ??= this.config.get('user');
14
- /* v8 ignore next: tableSpace fallback */
15
- const tableSpace = this.config.get('schemaGenerator').tableSpace ?? 'mikro_orm';
16
- const password = this.connection.mapOptions({}).password;
17
- try {
18
- await this.execute(
19
- `create tablespace ${this.platform.quoteIdentifier(tableSpace)} datafile '${tableSpace.replaceAll(`'`, `''`)}.dbf' size 100M autoextend on`,
20
- );
21
- } catch (e) {
22
- if (e.code !== 'ORA-01543') {
23
- throw e;
24
- }
4
+ /** Tracks whether the main user has been granted DBA (or equivalent) privileges. */
5
+ hasDbaGrant = false;
6
+ static register(orm) {
7
+ orm.config.registerExtension('@mikro-orm/schema-generator', () => new OracleSchemaGenerator(orm.em));
25
8
  }
26
- const sql = [
27
- `create user ${this.platform.quoteIdentifier(name)}`,
28
- `identified by ${this.platform.quoteIdentifier(password)}`,
29
- `default tablespace ${this.platform.quoteIdentifier(tableSpace)}`,
30
- `quota unlimited on ${this.platform.quoteIdentifier(tableSpace)}`,
31
- ].join(' ');
32
- await this.execute(sql);
33
- await this.execute(`grant connect, resource to ${this.platform.quoteIdentifier(name)}`);
34
- this.config.set('user', name);
35
- await this.driver.reconnect();
36
- }
37
- async dropDatabase(name) {
38
- name ??= this.config.get('dbName');
39
- this.config.set('user', this.helper.getManagementDbName());
40
- await this.driver.reconnect();
41
- await this.execute(this.helper.getDropDatabaseSQL(name));
42
- this.config.set('user', name);
43
- }
44
- /**
45
- * Oracle uses CASCADE CONSTRAINT in DROP TABLE and has no native enums,
46
- * so we can generate drop SQL from metadata alone — no DB introspection needed.
47
- */
48
- async getDropSchemaSQL(options = {}) {
49
- await this.ensureDatabase();
50
- const metadata = this.getOrderedMetadata(options.schema).reverse();
51
- const ret = [];
52
- for (const meta of metadata) {
53
- const schemaName = options.schema ?? this.config.get('schema');
54
- /* v8 ignore next: wildcard schema branch */
55
- const resolved = meta.schema === '*' ? schemaName : (meta.schema ?? schemaName);
56
- /* v8 ignore next: default schema resolution */
57
- const schema = resolved === this.platform.getDefaultSchemaName() ? undefined : resolved;
58
- this.helper.append(ret, this.helper.dropTableIfExists(meta.tableName, schema));
59
- }
60
- if (options.dropMigrationsTable) {
61
- this.helper.append(
62
- ret,
63
- this.helper.dropTableIfExists(this.config.get('migrations').tableName, this.config.get('schema')),
64
- );
65
- }
66
- /* v8 ignore next: empty result branch */
67
- return ret.join('\n') + (ret.length ? '\n' : '');
68
- }
69
- async ensureDatabase(options) {
70
- const dbName = this.config.get('dbName');
71
- if (this.lastEnsuredDatabase === dbName && !options?.forceCheck) {
72
- return true;
9
+ /**
10
+ * creates new database and connects to it
11
+ */
12
+ async createDatabase(name) {
13
+ name ??= this.config.get('user');
14
+ /* v8 ignore next: tableSpace fallback */
15
+ const tableSpace = this.config.get('schemaGenerator').tableSpace ?? 'mikro_orm';
16
+ const password = this.connection.mapOptions({}).password;
17
+ try {
18
+ await this.execute(`create tablespace ${this.platform.quoteIdentifier(tableSpace)} datafile '${tableSpace.replaceAll(`'`, `''`)}.dbf' size 100M autoextend on`);
19
+ }
20
+ catch (e) {
21
+ if (e.code !== 'ORA-01543') {
22
+ throw e;
23
+ }
24
+ }
25
+ const sql = [
26
+ `create user ${this.platform.quoteIdentifier(name)}`,
27
+ `identified by ${this.platform.quoteIdentifier(password)}`,
28
+ `default tablespace ${this.platform.quoteIdentifier(tableSpace)}`,
29
+ `quota unlimited on ${this.platform.quoteIdentifier(tableSpace)}`,
30
+ ].join(' ');
31
+ await this.execute(sql);
32
+ await this.execute(`grant connect, resource to ${this.platform.quoteIdentifier(name)}`);
33
+ this.config.set('user', name);
34
+ await this.driver.reconnect();
73
35
  }
74
- let exists = false;
75
- try {
76
- exists = await this.helper.databaseExists(this.connection, dbName);
77
- } catch (e) {
78
- if (e.code === 'ORA-01017' && this.config.get('user') !== this.helper.getManagementDbName()) {
36
+ async dropDatabase(name) {
37
+ name ??= this.config.get('dbName');
79
38
  this.config.set('user', this.helper.getManagementDbName());
80
39
  await this.driver.reconnect();
81
- const result = await this.ensureDatabase();
82
- // Restore connection to the original user (createDatabase does this
83
- // when the user doesn't exist, but we must handle the case where
84
- // the user already exists and ensureDatabase returned early)
85
- if (this.config.get('user') !== dbName) {
86
- this.config.set('user', dbName);
87
- await this.driver.reconnect();
88
- }
89
- return result;
90
- }
91
- throw e;
92
- }
93
- this.lastEnsuredDatabase = dbName;
94
- if (!exists) {
95
- this.config.set('user', this.helper.getManagementDbName());
96
- await this.driver.reconnect();
97
- await this.createDatabase(dbName);
98
- if (options?.create) {
99
- await this.create(options);
100
- }
101
- return true;
40
+ await this.execute(this.helper.getDropDatabaseSQL(name));
41
+ this.config.set('user', name);
102
42
  }
103
- if (options?.clear) {
104
- await this.clear(options);
105
- }
106
- return false;
107
- }
108
- getOriginalUser() {
109
- return this.config.get('user') ?? this.config.get('dbName');
110
- }
111
- /**
112
- * Connects as the management (system) user if not already connected as admin.
113
- * Returns the original user to restore later, or undefined if no reconnect was needed.
114
- */
115
- async connectAsAdmin() {
116
- if (this.hasDbaGrant) {
117
- return undefined;
43
+ /**
44
+ * Oracle uses CASCADE CONSTRAINT in DROP TABLE and has no native enums,
45
+ * so we can generate drop SQL from metadata alone — no DB introspection needed.
46
+ */
47
+ async getDropSchemaSQL(options = {}) {
48
+ await this.ensureDatabase();
49
+ const metadata = this.getOrderedMetadata(options.schema).reverse();
50
+ const ret = [];
51
+ for (const meta of metadata) {
52
+ const schemaName = options.schema ?? this.config.get('schema');
53
+ /* v8 ignore next: wildcard schema branch */
54
+ const resolved = meta.schema === '*' ? schemaName : (meta.schema ?? schemaName);
55
+ /* v8 ignore next: default schema resolution */
56
+ const schema = resolved === this.platform.getDefaultSchemaName() ? undefined : resolved;
57
+ this.helper.append(ret, this.helper.dropTableIfExists(meta.tableName, schema));
58
+ }
59
+ if (options.dropMigrationsTable) {
60
+ this.helper.append(ret, this.helper.dropTableIfExists(this.config.get('migrations').tableName, this.config.get('schema')));
61
+ }
62
+ /* v8 ignore next: empty result branch */
63
+ return ret.join('\n') + (ret.length ? '\n' : '');
118
64
  }
119
- // Check if the current user already has DBA privileges (avoids pool reconnect churn)
120
- const res = await this.connection.execute(`select granted_role from user_role_privs where granted_role = 'DBA'`);
121
- if (res.length > 0) {
122
- this.hasDbaGrant = true;
123
- return undefined;
65
+ async ensureDatabase(options) {
66
+ const dbName = this.config.get('dbName');
67
+ if (this.lastEnsuredDatabase === dbName && !options?.forceCheck) {
68
+ return true;
69
+ }
70
+ let exists = false;
71
+ try {
72
+ exists = await this.helper.databaseExists(this.connection, dbName);
73
+ }
74
+ catch (e) {
75
+ if (e.code === 'ORA-01017' && this.config.get('user') !== this.helper.getManagementDbName()) {
76
+ this.config.set('user', this.helper.getManagementDbName());
77
+ await this.driver.reconnect();
78
+ const result = await this.ensureDatabase();
79
+ // Restore connection to the original user (createDatabase does this
80
+ // when the user doesn't exist, but we must handle the case where
81
+ // the user already exists and ensureDatabase returned early)
82
+ if (this.config.get('user') !== dbName) {
83
+ this.config.set('user', dbName);
84
+ await this.driver.reconnect();
85
+ }
86
+ return result;
87
+ }
88
+ throw e;
89
+ }
90
+ this.lastEnsuredDatabase = dbName;
91
+ if (!exists) {
92
+ this.config.set('user', this.helper.getManagementDbName());
93
+ await this.driver.reconnect();
94
+ await this.createDatabase(dbName);
95
+ if (options?.create) {
96
+ await this.create(options);
97
+ }
98
+ return true;
99
+ }
100
+ if (options?.clear) {
101
+ await this.clear(options);
102
+ }
103
+ return false;
124
104
  }
125
- const originalUser = this.getOriginalUser();
126
- this.config.set('user', this.helper.getManagementDbName());
127
- await this.driver.reconnect();
128
- return originalUser;
129
- }
130
- /**
131
- * Restores the connection to the original user after admin operations.
132
- */
133
- async restoreConnection(originalUser) {
134
- if (originalUser == null) {
135
- return;
105
+ getOriginalUser() {
106
+ return this.config.get('user') ?? this.config.get('dbName');
136
107
  }
137
- this.config.set('user', originalUser);
138
- await this.driver.reconnect();
139
- }
140
- /**
141
- * Grants DBA (or fallback individual privileges) to the main user.
142
- * Only executed once — subsequent calls are no-ops.
143
- */
144
- async ensureDbaGrant(originalUser) {
145
- if (this.hasDbaGrant) {
146
- return;
108
+ /**
109
+ * Connects as the management (system) user if not already connected as admin.
110
+ * Returns the original user to restore later, or undefined if no reconnect was needed.
111
+ */
112
+ async connectAsAdmin() {
113
+ if (this.hasDbaGrant) {
114
+ return undefined;
115
+ }
116
+ // Check if the current user already has DBA privileges (avoids pool reconnect churn)
117
+ const res = await this.connection.execute(`select granted_role from user_role_privs where granted_role = 'DBA'`);
118
+ if (res.length > 0) {
119
+ this.hasDbaGrant = true;
120
+ return undefined;
121
+ }
122
+ const originalUser = this.getOriginalUser();
123
+ this.config.set('user', this.helper.getManagementDbName());
124
+ await this.driver.reconnect();
125
+ return originalUser;
147
126
  }
148
- try {
149
- await this.execute(`grant dba to ${this.platform.quoteIdentifier(originalUser)}`);
150
- } catch {
151
- await this.execute(`grant create any table to ${this.platform.quoteIdentifier(originalUser)}`);
152
- await this.execute(`grant alter any table to ${this.platform.quoteIdentifier(originalUser)}`);
153
- await this.execute(`grant drop any table to ${this.platform.quoteIdentifier(originalUser)}`);
154
- await this.execute(`grant create any index to ${this.platform.quoteIdentifier(originalUser)}`);
155
- await this.execute(`grant drop any index to ${this.platform.quoteIdentifier(originalUser)}`);
127
+ /**
128
+ * Restores the connection to the original user after admin operations.
129
+ */
130
+ async restoreConnection(originalUser) {
131
+ if (originalUser == null) {
132
+ return;
133
+ }
134
+ this.config.set('user', originalUser);
135
+ await this.driver.reconnect();
156
136
  }
157
- this.hasDbaGrant = true;
158
- }
159
- async createNamespace(name) {
160
- const originalUser = this.getOriginalUser();
161
- const reconnectUser = await this.connectAsAdmin();
162
- try {
163
- await this.execute(this.helper.getCreateNamespaceSQL(name));
164
- } catch (e) {
165
- /* v8 ignore next 3: unexpected createNamespace error rethrow */
166
- if (e.code !== 'ORA-01920') {
167
- throw e;
168
- }
137
+ /**
138
+ * Grants DBA (or fallback individual privileges) to the main user.
139
+ * Only executed once — subsequent calls are no-ops.
140
+ */
141
+ async ensureDbaGrant(originalUser) {
142
+ if (this.hasDbaGrant) {
143
+ return;
144
+ }
145
+ try {
146
+ await this.execute(`grant dba to ${this.platform.quoteIdentifier(originalUser)}`);
147
+ }
148
+ catch {
149
+ await this.execute(`grant create any table to ${this.platform.quoteIdentifier(originalUser)}`);
150
+ await this.execute(`grant alter any table to ${this.platform.quoteIdentifier(originalUser)}`);
151
+ await this.execute(`grant drop any table to ${this.platform.quoteIdentifier(originalUser)}`);
152
+ await this.execute(`grant create any index to ${this.platform.quoteIdentifier(originalUser)}`);
153
+ await this.execute(`grant drop any index to ${this.platform.quoteIdentifier(originalUser)}`);
154
+ }
155
+ this.hasDbaGrant = true;
169
156
  }
170
- await this.execute(`grant connect, resource to ${this.platform.quoteIdentifier(name)}`);
171
- await this.ensureDbaGrant(originalUser);
172
- await this.grantReferencesForSchema(name);
173
- await this.restoreConnection(reconnectUser);
174
- }
175
- async dropNamespace(name) {
176
- const reconnectUser = await this.connectAsAdmin();
177
- // Try drop first; only kill sessions if the user is currently connected
178
- try {
179
- await this.execute(this.helper.getDropNamespaceSQL(name));
180
- } catch (e) {
181
- if (e.code === 'ORA-01918') {
182
- // User does not exist — nothing to do
183
- } else if (e.code === 'ORA-01940') {
157
+ async createNamespace(name) {
158
+ const originalUser = this.getOriginalUser();
159
+ const reconnectUser = await this.connectAsAdmin();
184
160
  try {
185
- const sessions = await this.connection.execute(
186
- `select sid, serial# as "serial" from v$session where username = ${this.platform.quoteValue(name.toUpperCase())}`,
187
- );
188
- for (const session of sessions) {
189
- try {
190
- await this.execute(`alter system kill session '${session.sid},${session.serial}' immediate`);
191
- } catch (e3) {
192
- this.config
193
- .getLogger()
194
- .warn('schema', `Failed to kill session ${session.sid},${session.serial}: ${e3.message}`);
161
+ await this.execute(this.helper.getCreateNamespaceSQL(name));
162
+ }
163
+ catch (e) {
164
+ /* v8 ignore next 3: unexpected createNamespace error rethrow */
165
+ if (e.code !== 'ORA-01920') {
166
+ throw e;
195
167
  }
196
- }
197
- } catch (e3) {
198
- this.config.getLogger().warn('schema', `Cannot query v$session: ${e3.message}`);
199
168
  }
200
- try {
201
- await this.execute(this.helper.getDropNamespaceSQL(name));
202
- } catch (e2) {
203
- if (e2.code !== 'ORA-01918' && e2.code !== 'ORA-01940') {
204
- throw e2;
205
- }
206
- }
207
- } else {
208
- throw e;
209
- }
169
+ await this.execute(`grant connect, resource to ${this.platform.quoteIdentifier(name)}`);
170
+ await this.ensureDbaGrant(originalUser);
171
+ await this.grantReferencesForSchema(name);
172
+ await this.restoreConnection(reconnectUser);
210
173
  }
211
- await this.restoreConnection(reconnectUser);
212
- }
213
- async update(options = {}) {
214
- await this.ensureDatabase();
215
- options.safe ??= false;
216
- options.dropTables ??= true;
217
- const toSchema = this.getTargetSchema(options.schema);
218
- const schemas = toSchema.getNamespaces();
219
- const fromSchema =
220
- options.fromSchema ??
221
- (await DatabaseSchema.create(
222
- this.connection,
223
- this.platform,
224
- this.config,
225
- options.schema,
226
- schemas,
227
- undefined,
228
- this.options.skipTables,
229
- this.options.skipViews,
230
- ));
231
- const wildcardSchemaTables = [...this.metadata.getAll().values()]
232
- .filter(meta => meta.schema === '*')
233
- .map(/* v8 ignore next */ /* v8 ignore next */ meta => meta.tableName);
234
- fromSchema.prune(options.schema, wildcardSchemaTables);
235
- toSchema.prune(options.schema, wildcardSchemaTables);
236
- const comparator = new SchemaComparator(this.platform);
237
- const diff = comparator.compare(fromSchema, toSchema);
238
- // Phase 1: Create namespaces — requires elevated privileges.
239
- // After granting DBA, we reconnect back to the original user so that
240
- // unqualified object names (e.g. indexes) are created in the correct schema.
241
- /* v8 ignore start: requires multi-schema Oracle setup */
242
- if (diff.newNamespaces.size > 0) {
243
- const originalUser = this.getOriginalUser();
244
- const reconnectUser = await this.connectAsAdmin();
245
- for (const ns of diff.newNamespaces) {
174
+ async dropNamespace(name) {
175
+ const reconnectUser = await this.connectAsAdmin();
176
+ // Try drop first; only kill sessions if the user is currently connected
246
177
  try {
247
- await this.execute(this.helper.getCreateNamespaceSQL(ns));
248
- } catch (e) {
249
- if (e.code !== 'ORA-01920') {
250
- throw e;
251
- }
252
- }
253
- await this.execute(`grant connect, resource to ${this.platform.quoteIdentifier(ns)}`);
254
- }
255
- await this.ensureDbaGrant(originalUser);
256
- for (const ns of diff.newNamespaces) {
257
- await this.grantReferencesForSchema(ns);
258
- }
259
- await this.restoreConnection(reconnectUser);
260
- }
261
- /* v8 ignore stop */
262
- // Phase 2: Execute table creation and alterations (without FKs)
263
- // Build SQL without FK-related parts, and clear newNamespaces since we handled them in Phase 1
264
- const tableOnlyDiff = { ...diff, orphanedForeignKeys: [], newNamespaces: new Set() };
265
- const savedChangedTables = { ...diff.changedTables };
266
- // Strip FK additions from changedTables for Phase 2
267
- // Keep removedForeignKeys so old FKs are dropped before column changes (via preAlterTable)
268
- // changedForeignKeys are dropped in preAlterTable but re-created in alterTable,
269
- // so we move them to removedForeignKeys for drop-only and handle re-creation in Phase 3
270
- const strippedChangedTables = {};
271
- for (const [key, table] of Object.entries(savedChangedTables)) {
272
- strippedChangedTables[key] = {
273
- ...table,
274
- addedForeignKeys: {},
275
- changedForeignKeys: {},
276
- removedForeignKeys: {
277
- ...table.removedForeignKeys,
278
- ...table.changedForeignKeys,
279
- },
280
- };
281
- }
282
- tableOnlyDiff.changedTables = strippedChangedTables;
283
- // Temporarily override getForeignKeys on new tables to suppress FK constraints in Phase 2
284
- // (Oracle requires REFERENCES grants before FK creation, handled in Phase 2.5 / Phase 3)
285
- const originalGetForeignKeys = new Map();
286
- for (const table of Object.values(diff.newTables)) {
287
- originalGetForeignKeys.set(table, table.getForeignKeys.bind(table));
288
- table.getForeignKeys = () => ({});
289
- }
290
- tableOnlyDiff.newTables = diff.newTables;
291
- const tableSQL = this.diffToSQL(tableOnlyDiff, options);
292
- // Restore original getForeignKeys
293
- for (const [table, origFn] of originalGetForeignKeys) {
294
- table.getForeignKeys = origFn;
295
- }
296
- if (tableSQL) {
297
- await this.execute(tableSQL);
298
- }
299
- // Phase 2.5: Grant REFERENCES on newly created tables to all other schemas
300
- const allSchemas = [...schemas];
301
- for (const table of Object.values(diff.newTables)) {
302
- /* v8 ignore next: schema fallback chain */
303
- const tableSchema = table.schema ?? this.platform.getDefaultSchemaName() ?? '';
304
- for (const schema of allSchemas) {
305
- if (schema === tableSchema || schema === this.platform.getDefaultSchemaName()) {
306
- continue;
178
+ await this.execute(this.helper.getDropNamespaceSQL(name));
179
+ }
180
+ catch (e) {
181
+ if (e.code === 'ORA-01918') {
182
+ // User does not exist — nothing to do
183
+ }
184
+ else if (e.code === 'ORA-01940') {
185
+ try {
186
+ const sessions = await this.connection.execute(`select sid, serial# as "serial" from v$session where username = ${this.platform.quoteValue(name.toUpperCase())}`);
187
+ for (const session of sessions) {
188
+ try {
189
+ await this.execute(`alter system kill session '${session.sid},${session.serial}' immediate`);
190
+ }
191
+ catch (e3) {
192
+ this.config
193
+ .getLogger()
194
+ .warn('schema', `Failed to kill session ${session.sid},${session.serial}: ${e3.message}`);
195
+ }
196
+ }
197
+ }
198
+ catch (e3) {
199
+ this.config.getLogger().warn('schema', `Cannot query v$session: ${e3.message}`);
200
+ }
201
+ try {
202
+ await this.execute(this.helper.getDropNamespaceSQL(name));
203
+ }
204
+ catch (e2) {
205
+ if (e2.code !== 'ORA-01918' && e2.code !== 'ORA-01940') {
206
+ throw e2;
207
+ }
208
+ }
209
+ }
210
+ else {
211
+ throw e;
212
+ }
307
213
  }
214
+ await this.restoreConnection(reconnectUser);
215
+ }
216
+ async update(options = {}) {
217
+ await this.ensureDatabase();
218
+ options.safe ??= false;
219
+ options.dropTables ??= true;
220
+ const toSchema = this.getTargetSchema(options.schema);
221
+ const schemas = toSchema.getNamespaces();
222
+ const fromSchema = options.fromSchema ??
223
+ (await DatabaseSchema.create(this.connection, this.platform, this.config, options.schema, schemas, undefined, this.options.skipTables, this.options.skipViews));
224
+ const wildcardSchemaTables = [...this.metadata.getAll().values()]
225
+ .filter(meta => meta.schema === '*')
226
+ .map(/* v8 ignore next */ /* v8 ignore next */ meta => meta.tableName);
227
+ fromSchema.prune(options.schema, wildcardSchemaTables);
228
+ toSchema.prune(options.schema, wildcardSchemaTables);
229
+ const comparator = new SchemaComparator(this.platform);
230
+ const diff = comparator.compare(fromSchema, toSchema);
231
+ // Phase 1: Create namespaces — requires elevated privileges.
232
+ // After granting DBA, we reconnect back to the original user so that
233
+ // unqualified object names (e.g. indexes) are created in the correct schema.
308
234
  /* v8 ignore start: requires multi-schema Oracle setup */
309
- try {
310
- await this.execute(
311
- `grant references on ${this.platform.quoteIdentifier(tableSchema)}.${this.platform.quoteIdentifier(table.name)} to ${this.platform.quoteIdentifier(schema)}`,
312
- );
313
- } catch {
314
- // ignore errors (e.g., table doesn't exist yet in that schema)
235
+ if (diff.newNamespaces.size > 0) {
236
+ const originalUser = this.getOriginalUser();
237
+ const reconnectUser = await this.connectAsAdmin();
238
+ for (const ns of diff.newNamespaces) {
239
+ try {
240
+ await this.execute(this.helper.getCreateNamespaceSQL(ns));
241
+ }
242
+ catch (e) {
243
+ if (e.code !== 'ORA-01920') {
244
+ throw e;
245
+ }
246
+ }
247
+ await this.execute(`grant connect, resource to ${this.platform.quoteIdentifier(ns)}`);
248
+ }
249
+ await this.ensureDbaGrant(originalUser);
250
+ for (const ns of diff.newNamespaces) {
251
+ await this.grantReferencesForSchema(ns);
252
+ }
253
+ await this.restoreConnection(reconnectUser);
315
254
  }
316
255
  /* v8 ignore stop */
317
- }
318
- }
319
- // Phase 3: Execute FK creation for new tables and FK changes for altered tables
320
- const fkStatements = [];
321
- // FK constraints for new tables
322
- for (const table of Object.values(diff.newTables)) {
323
- for (const fk of Object.values(table.getForeignKeys())) {
324
- fkStatements.push(this.helper.createForeignKey(table, fk));
325
- }
326
- }
327
- // FK drop for orphaned foreign keys (FKs pointing to removed tables)
328
- /* v8 ignore next 5: orphaned FKs are cascade-dropped with their referenced tables in Phase 2 */
329
- for (const orphanedForeignKey of diff.orphanedForeignKeys) {
330
- const [schemaName, tableName] = this.helper.splitTableName(orphanedForeignKey.localTableName, true);
331
- const name = (schemaName ? schemaName + '.' : '') + tableName;
332
- fkStatements.push(this.helper.dropForeignKey(name, orphanedForeignKey.constraintName));
333
- }
334
- // FK additions and re-creation of changed FKs (drops already handled in Phase 2)
335
- /* v8 ignore next 9: FK change branches depend on schema diff state */
336
- for (const table of Object.values(savedChangedTables)) {
337
- for (const fk of Object.values(table.addedForeignKeys ?? {})) {
338
- fkStatements.push(this.helper.createForeignKey(table.toTable, fk));
339
- }
340
- for (const fk of Object.values(table.changedForeignKeys ?? {})) {
341
- fkStatements.push(this.helper.createForeignKey(table.toTable, fk));
342
- }
343
- }
344
- for (const stmt of fkStatements.filter(s => s)) {
345
- await this.execute(stmt);
346
- }
347
- }
348
- async clear(options) {
349
- // truncate by default, so no value is considered as true
350
- if (options?.truncate === false) {
351
- return super.clear(options);
352
- }
353
- const stmts = [];
354
- for (const meta of this.getOrderedMetadata(options?.schema).reverse()) {
355
- const schema =
356
- meta.schema && meta.schema !== '*'
357
- ? meta.schema
358
- : (options?.schema ?? this.config.get('schema', this.platform.getDefaultSchemaName()));
359
- const tableName = this.driver.getTableName(meta, { schema }, false);
360
- const quoted = tableName
361
- .split('.')
362
- .map(p => this.platform.quoteIdentifier(p))
363
- .join('.');
364
- // Use DELETE instead of TRUNCATE to avoid ORA-02266 (FK constraint check regardless of data)
365
- stmts.push(
366
- `begin execute immediate 'delete from ${quoted}'; exception when others then if sqlcode != -942 then raise; end if; end;`,
367
- );
368
- for (const pk of meta.getPrimaryProps().filter(p => p.autoincrement)) {
369
- stmts.push(
370
- `begin execute immediate 'alter table ${quoted} modify ${this.platform.quoteIdentifier(pk.fieldNames[0])} generated by default as identity (start with limit value)'; exception when others then null; end;`,
371
- );
372
- }
256
+ // Phase 2: Execute table creation and alterations (without FKs)
257
+ // Build SQL without FK-related parts, and clear newNamespaces since we handled them in Phase 1
258
+ const tableOnlyDiff = { ...diff, orphanedForeignKeys: [], newNamespaces: new Set() };
259
+ const savedChangedTables = { ...diff.changedTables };
260
+ // Strip FK additions from changedTables for Phase 2
261
+ // Keep removedForeignKeys so old FKs are dropped before column changes (via preAlterTable)
262
+ // changedForeignKeys are dropped in preAlterTable but re-created in alterTable,
263
+ // so we move them to removedForeignKeys for drop-only and handle re-creation in Phase 3
264
+ const strippedChangedTables = {};
265
+ for (const [key, table] of Object.entries(savedChangedTables)) {
266
+ strippedChangedTables[key] = {
267
+ ...table,
268
+ addedForeignKeys: {},
269
+ changedForeignKeys: {},
270
+ removedForeignKeys: {
271
+ ...table.removedForeignKeys,
272
+ ...table.changedForeignKeys,
273
+ },
274
+ };
275
+ }
276
+ tableOnlyDiff.changedTables = strippedChangedTables;
277
+ // Temporarily override getForeignKeys on new tables to suppress FK constraints in Phase 2
278
+ // (Oracle requires REFERENCES grants before FK creation, handled in Phase 2.5 / Phase 3)
279
+ const originalGetForeignKeys = new Map();
280
+ for (const table of Object.values(diff.newTables)) {
281
+ originalGetForeignKeys.set(table, table.getForeignKeys.bind(table));
282
+ table.getForeignKeys = () => ({});
283
+ }
284
+ tableOnlyDiff.newTables = diff.newTables;
285
+ const tableSQL = this.diffToSQL(tableOnlyDiff, options);
286
+ // Restore original getForeignKeys
287
+ for (const [table, origFn] of originalGetForeignKeys) {
288
+ table.getForeignKeys = origFn;
289
+ }
290
+ if (tableSQL) {
291
+ await this.execute(tableSQL);
292
+ }
293
+ // Phase 2.5: Grant REFERENCES on newly created tables to all other schemas
294
+ const allSchemas = [...schemas];
295
+ for (const table of Object.values(diff.newTables)) {
296
+ /* v8 ignore next: schema fallback chain */
297
+ const tableSchema = table.schema ?? this.platform.getDefaultSchemaName() ?? '';
298
+ for (const schema of allSchemas) {
299
+ if (schema === tableSchema || schema === this.platform.getDefaultSchemaName()) {
300
+ continue;
301
+ }
302
+ /* v8 ignore start: requires multi-schema Oracle setup */
303
+ try {
304
+ await this.execute(`grant references on ${this.platform.quoteIdentifier(tableSchema)}.${this.platform.quoteIdentifier(table.name)} to ${this.platform.quoteIdentifier(schema)}`);
305
+ }
306
+ catch {
307
+ // ignore errors (e.g., table doesn't exist yet in that schema)
308
+ }
309
+ /* v8 ignore stop */
310
+ }
311
+ }
312
+ // Phase 3: Execute FK creation for new tables and FK changes for altered tables
313
+ const fkStatements = [];
314
+ // FK constraints for new tables
315
+ for (const table of Object.values(diff.newTables)) {
316
+ for (const fk of Object.values(table.getForeignKeys())) {
317
+ fkStatements.push(this.helper.createForeignKey(table, fk));
318
+ }
319
+ }
320
+ // FK drop for orphaned foreign keys (FKs pointing to removed tables)
321
+ /* v8 ignore next 5: orphaned FKs are cascade-dropped with their referenced tables in Phase 2 */
322
+ for (const orphanedForeignKey of diff.orphanedForeignKeys) {
323
+ const [schemaName, tableName] = this.helper.splitTableName(orphanedForeignKey.localTableName, true);
324
+ const name = (schemaName ? schemaName + '.' : '') + tableName;
325
+ fkStatements.push(this.helper.dropForeignKey(name, orphanedForeignKey.constraintName));
326
+ }
327
+ // FK additions and re-creation of changed FKs (drops already handled in Phase 2)
328
+ /* v8 ignore next 9: FK change branches depend on schema diff state */
329
+ for (const table of Object.values(savedChangedTables)) {
330
+ for (const fk of Object.values(table.addedForeignKeys ?? {})) {
331
+ fkStatements.push(this.helper.createForeignKey(table.toTable, fk));
332
+ }
333
+ for (const fk of Object.values(table.changedForeignKeys ?? {})) {
334
+ fkStatements.push(this.helper.createForeignKey(table.toTable, fk));
335
+ }
336
+ }
337
+ for (const stmt of fkStatements.filter(s => s)) {
338
+ await this.execute(stmt);
339
+ }
373
340
  }
374
- /* v8 ignore next 5: empty stmts branch */
375
- if (stmts.length > 0) {
376
- // Use driver.execute directly to bypass the ;\n splitting in this.execute()
377
- // DELETE is DML (not DDL), so we must commit explicitly
378
- await this.driver.execute(`begin ${stmts.join(' ')} commit; end;`);
341
+ async clear(options) {
342
+ // truncate by default, so no value is considered as true
343
+ if (options?.truncate === false) {
344
+ return super.clear(options);
345
+ }
346
+ const stmts = [];
347
+ for (const meta of this.getOrderedMetadata(options?.schema).reverse()) {
348
+ const schema = meta.schema && meta.schema !== '*'
349
+ ? meta.schema
350
+ : (options?.schema ?? this.config.get('schema', this.platform.getDefaultSchemaName()));
351
+ const tableName = this.driver.getTableName(meta, { schema }, false);
352
+ const quoted = tableName
353
+ .split('.')
354
+ .map(p => this.platform.quoteIdentifier(p))
355
+ .join('.');
356
+ // Use DELETE instead of TRUNCATE to avoid ORA-02266 (FK constraint check regardless of data)
357
+ stmts.push(`begin execute immediate 'delete from ${quoted}'; exception when others then if sqlcode != -942 then raise; end if; end;`);
358
+ for (const pk of meta.getPrimaryProps().filter(p => p.autoincrement)) {
359
+ stmts.push(`begin execute immediate 'alter table ${quoted} modify ${this.platform.quoteIdentifier(pk.fieldNames[0])} generated by default as identity (start with limit value)'; exception when others then null; end;`);
360
+ }
361
+ }
362
+ /* v8 ignore next 5: empty stmts branch */
363
+ if (stmts.length > 0) {
364
+ // Use driver.execute directly to bypass the ;\n splitting in this.execute()
365
+ // DELETE is DML (not DDL), so we must commit explicitly
366
+ await this.driver.execute(`begin ${stmts.join(' ')} commit; end;`);
367
+ }
368
+ this.clearIdentityMap();
379
369
  }
380
- this.clearIdentityMap();
381
- }
382
- async grantReferencesForSchema(schemaName) {
383
- const defaultSchema = this.platform.getDefaultSchemaName();
384
- const allSchemas = [...this.getTargetSchema().getNamespaces()];
385
- for (const otherSchema of allSchemas) {
386
- if (otherSchema === schemaName || otherSchema === defaultSchema) {
387
- continue;
388
- }
389
- // Get tables in the other schema and grant REFERENCES to the new schema
390
- const tables = await this.connection.execute(
391
- `select table_name from all_tables where owner = ${this.platform.quoteValue(otherSchema)}`,
392
- );
393
- for (const table of tables) {
394
- try {
395
- await this.execute(
396
- `grant references on ${this.platform.quoteIdentifier(otherSchema)}.${this.platform.quoteIdentifier(table.table_name)} to ${this.platform.quoteIdentifier(schemaName)}`,
397
- );
398
- } catch {
399
- // ignore errors
400
- }
401
- }
402
- // Also grant REFERENCES on new schema's tables to the other schema
403
- const newTables = await this.connection.execute(
404
- `select table_name from all_tables where owner = ${this.platform.quoteValue(schemaName)}`,
405
- );
406
- for (const table of newTables) {
407
- try {
408
- await this.execute(
409
- `grant references on ${this.platform.quoteIdentifier(schemaName)}.${this.platform.quoteIdentifier(table.table_name)} to ${this.platform.quoteIdentifier(otherSchema)}`,
410
- );
411
- } catch {
412
- // ignore errors
370
+ async grantReferencesForSchema(schemaName) {
371
+ const defaultSchema = this.platform.getDefaultSchemaName();
372
+ const allSchemas = [...this.getTargetSchema().getNamespaces()];
373
+ for (const otherSchema of allSchemas) {
374
+ if (otherSchema === schemaName || otherSchema === defaultSchema) {
375
+ continue;
376
+ }
377
+ // Get tables in the other schema and grant REFERENCES to the new schema
378
+ const tables = await this.connection.execute(`select table_name from all_tables where owner = ${this.platform.quoteValue(otherSchema)}`);
379
+ for (const table of tables) {
380
+ try {
381
+ await this.execute(`grant references on ${this.platform.quoteIdentifier(otherSchema)}.${this.platform.quoteIdentifier(table.table_name)} to ${this.platform.quoteIdentifier(schemaName)}`);
382
+ }
383
+ catch {
384
+ // ignore errors
385
+ }
386
+ }
387
+ // Also grant REFERENCES on new schema's tables to the other schema
388
+ const newTables = await this.connection.execute(`select table_name from all_tables where owner = ${this.platform.quoteValue(schemaName)}`);
389
+ for (const table of newTables) {
390
+ try {
391
+ await this.execute(`grant references on ${this.platform.quoteIdentifier(schemaName)}.${this.platform.quoteIdentifier(table.table_name)} to ${this.platform.quoteIdentifier(otherSchema)}`);
392
+ }
393
+ catch {
394
+ // ignore errors
395
+ }
396
+ }
413
397
  }
414
- }
415
398
  }
416
- }
417
399
  }