@mikro-orm/sql 7.2.0-dev.0 → 7.2.0-dev.10

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.
@@ -36,6 +36,13 @@ export declare abstract class AbstractSqlConnection extends Connection {
36
36
  getClient<T = any>(): Kysely<T>;
37
37
  /** Ensures the Kysely client is initialized, creating it asynchronously if needed. */
38
38
  initClient(): Promise<void>;
39
+ /**
40
+ * Guards `getNativeClient()` overrides — drivers only capture their client while building their
41
+ * own dialect, which `driverOptions` carrying a ready-made Kysely instance or dialect skips.
42
+ *
43
+ * @internal
44
+ */
45
+ protected requireNativeClient<T>(client: T | undefined | null): T;
39
46
  /** Executes a callback within a transaction, committing on success and rolling back on error. */
40
47
  transactional<T>(cb: (trx: Transaction<ControlledTransaction<any, any>>) => Promise<T>, options?: {
41
48
  isolationLevel?: IsolationLevel;
@@ -56,11 +63,20 @@ export declare abstract class AbstractSqlConnection extends Connection {
56
63
  commit(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
57
64
  /** Rolls back the transaction or rolls back to the savepoint. */
58
65
  rollback(ctx: ControlledTransaction<any, any>, eventBroadcaster?: TransactionEventBroadcaster, loggerContext?: LogContext): Promise<void>;
66
+ /**
67
+ * Waits until the transaction's connection has no query in flight. Kysely runs `rollback` straight
68
+ * on that connection instead of going through its connection provider, so a rollback caused by an
69
+ * aborted query would otherwise be sent while the aborted query is still running. That not only
70
+ * queues the rollback behind it on the server, it also overwrites the query id Kysely compares
71
+ * against before firing the `'cancel query'`/`'kill session'` control statement — the control
72
+ * statement is then discarded as stale and the abort never reaches the database.
73
+ */
74
+ private waitForIdleTransaction;
59
75
  private prepareQuery;
60
76
  /** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
61
- 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>;
77
+ execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[] | Dictionary<unknown>, method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
62
78
  /** Executes a SQL query and returns an async iterable that yields results row by row. */
63
- stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions, chunkSize?: number): AsyncIterableIterator<T>;
79
+ stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[] | Dictionary<unknown>, ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions, chunkSize?: number): AsyncIterableIterator<T>;
64
80
  /** @inheritDoc */
65
81
  executeDump(dump: string): Promise<void>;
66
82
  protected getSql(query: string, formatted: string, context?: LogContext): string;
@@ -1,5 +1,5 @@
1
1
  import { CompiledQuery, Kysely } from 'kysely';
2
- import { Connection, EventType, isRaw, Utils, } from '@mikro-orm/core';
2
+ import { Connection, EventType, isRaw, raw, Utils, } from '@mikro-orm/core';
3
3
  import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
4
4
  /**
5
5
  * Pulls cancellation controls out of a `loggerContext` payload, returning the abort options
@@ -101,6 +101,18 @@ export class AbstractSqlConnection extends Connection {
101
101
  await this.createKysely();
102
102
  }
103
103
  }
104
+ /**
105
+ * Guards `getNativeClient()` overrides — drivers only capture their client while building their
106
+ * own dialect, which `driverOptions` carrying a ready-made Kysely instance or dialect skips.
107
+ *
108
+ * @internal
109
+ */
110
+ requireNativeClient(client) {
111
+ if (client == null) {
112
+ throw new Error('The native client is not available, as it is owned by the Kysely instance or dialect passed via `driverOptions`. Access it through the object you provided there instead.');
113
+ }
114
+ return client;
115
+ }
104
116
  /** Executes a callback within a transaction, committing on success and rolling back on error. */
105
117
  async transactional(cb, options = {}) {
106
118
  const trx = await this.begin(options);
@@ -110,7 +122,15 @@ export class AbstractSqlConnection extends Connection {
110
122
  return ret;
111
123
  }
112
124
  catch (error) {
113
- await this.rollback(trx, options.eventBroadcaster, options.loggerContext);
125
+ // A failing rollback must not mask why the transaction failed in the first place — the
126
+ // `'kill session'` abort strategy tears the connection down, so the rollback that follows can
127
+ // only ever report the dead connection.
128
+ try {
129
+ await this.rollback(trx, options.eventBroadcaster, options.loggerContext);
130
+ }
131
+ catch (rollbackError) {
132
+ this.logger.warn('query', `Failed to roll back transaction: ${rollbackError.message}`);
133
+ }
114
134
  throw error;
115
135
  }
116
136
  }
@@ -138,18 +158,8 @@ export class AbstractSqlConnection extends Connection {
138
158
  trxBuilder = trxBuilder.setAccessMode('read only');
139
159
  }
140
160
  const trx = await trxBuilder.execute();
141
- if (options.ctx) {
142
- const ctx = options.ctx;
143
- ctx.index ??= 0;
144
- const savepointName = `trx${ctx.index + 1}`;
145
- Reflect.defineProperty(trx, 'index', { value: ctx.index + 1 });
146
- Reflect.defineProperty(trx, 'savepointName', { value: savepointName });
147
- this.logQuery(this.platform.getSavepointSQL(savepointName), options.loggerContext);
148
- }
149
- else {
150
- for (const query of this.platform.getBeginTransactionSQL(options)) {
151
- this.logQuery(query, options.loggerContext);
152
- }
161
+ for (const query of this.platform.getBeginTransactionSQL(options)) {
162
+ this.logQuery(query, options.loggerContext);
153
163
  }
154
164
  await options.eventBroadcaster?.dispatchEvent(EventType.afterTransactionStart, trx);
155
165
  return trx;
@@ -173,6 +183,7 @@ export class AbstractSqlConnection extends Connection {
173
183
  /** Rolls back the transaction or rolls back to the savepoint. */
174
184
  async rollback(ctx, eventBroadcaster, loggerContext) {
175
185
  await eventBroadcaster?.dispatchEvent(EventType.beforeTransactionRollback, ctx);
186
+ await this.waitForIdleTransaction(ctx);
176
187
  if ('savepointName' in ctx) {
177
188
  await ctx.rollbackToSavepoint(ctx.savepointName).execute();
178
189
  this.logQuery(this.platform.getRollbackToSavepointSQL(ctx.savepointName), loggerContext);
@@ -183,17 +194,32 @@ export class AbstractSqlConnection extends Connection {
183
194
  }
184
195
  await eventBroadcaster?.dispatchEvent(EventType.afterTransactionRollback, ctx);
185
196
  }
197
+ /**
198
+ * Waits until the transaction's connection has no query in flight. Kysely runs `rollback` straight
199
+ * on that connection instead of going through its connection provider, so a rollback caused by an
200
+ * aborted query would otherwise be sent while the aborted query is still running. That not only
201
+ * queues the rollback behind it on the server, it also overwrites the query id Kysely compares
202
+ * against before firing the `'cancel query'`/`'kill session'` control statement — the control
203
+ * statement is then discarded as stale and the abort never reaches the database.
204
+ */
205
+ async waitForIdleTransaction(ctx) {
206
+ await ctx.getExecutor().provideConnection(async () => undefined);
207
+ }
186
208
  prepareQuery(query, params = []) {
187
209
  if (query instanceof NativeQueryBuilder) {
188
210
  query = query.toRaw();
189
211
  }
212
+ if (typeof query === 'string' && !Array.isArray(params)) {
213
+ // plain object params hold named parameters, translate them via the `raw()` helper
214
+ query = raw(query, params);
215
+ }
190
216
  if (isRaw(query)) {
191
217
  params = query.params;
192
218
  query = query.sql;
193
219
  }
194
220
  query = this.config.get('onQuery')(query, params);
195
221
  const formatted = this.platform.formatQuery(query, params);
196
- return { query, params, formatted };
222
+ return { query, params: params, formatted };
197
223
  }
198
224
  /** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
199
225
  async execute(query, params = [], method = 'all', ctx, loggerContext) {
@@ -227,7 +253,7 @@ export class AbstractSqlConnection extends Connection {
227
253
  .stream(compiled, chunkSize ?? 100, abort ? { signal: abort.signal } : undefined);
228
254
  this.logQuery(sql, {
229
255
  sql,
230
- params,
256
+ params: q.params,
231
257
  ...cleanCtx,
232
258
  affected: Utils.isPlainObject(res) ? res.affectedRows : undefined,
233
259
  });
@@ -238,7 +264,7 @@ export class AbstractSqlConnection extends Connection {
238
264
  }
239
265
  }
240
266
  catch (e) {
241
- this.logQuery(sql, { sql, params, ...cleanCtx, level: 'error' });
267
+ this.logQuery(sql, { sql, params: q.params, ...cleanCtx, level: 'error' });
242
268
  throw e;
243
269
  }
244
270
  }
@@ -100,7 +100,7 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
100
100
  */
101
101
  private convertOwnerPksForPivotQuery;
102
102
  private getPivotOrderBy;
103
- execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
103
+ execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[] | Dictionary, method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
104
104
  stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>): AsyncIterableIterator<T>;
105
105
  /**
106
106
  * 1:1 owner side needs to be marked for population so QB auto-joins the owner id
@@ -168,7 +168,7 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
168
168
  protected extractManyToMany<T>(meta: EntityMetadata<T>, data: EntityDictionary<T>): EntityData<T>;
169
169
  protected processManyToMany<T extends object>(meta: EntityMetadata<T>, pks: Primary<T>[], collections: EntityData<T>, clear: boolean, options?: DriverMethodOptions): Promise<void>;
170
170
  lockPessimistic<T extends object>(entity: T, options: LockOptions): Promise<void>;
171
- protected buildPopulateWhere<T extends object>(meta: EntityMetadata<T>, joinedProps: PopulateOptions<T>[], options: Pick<FindOptions<any>, 'populateWhere'>): ObjectQuery<T>;
171
+ protected buildPopulateWhere<T extends object>(meta: EntityMetadata<T>, joinedProps: PopulateOptions<T>[], options: Pick<FindOptions<any>, 'populateWhere' | 'strategy'>): ObjectQuery<T>;
172
172
  /**
173
173
  * Builds a UNION ALL (or UNION) subquery from `unionWhere` branches and merges it
174
174
  * into the main WHERE as `pk IN (branch_1 UNION ALL branch_2 ...)`.
@@ -604,7 +604,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
604
604
  let pk;
605
605
  if (meta.primaryKeys.length > 1) {
606
606
  // owner has composite pk
607
- pk = Utils.getPrimaryKeyCond(data, meta.primaryKeys);
607
+ pk = Utils.getOrderedPrimaryKeys(data, meta);
608
608
  }
609
609
  else {
610
610
  /* v8 ignore next */
@@ -887,10 +887,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
887
887
  }
888
888
  const res = await this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options));
889
889
  let pk;
890
- /* v8 ignore next */
891
890
  if (pks.length > 1) {
892
891
  // owner has composite pk
893
- pk = data.map(d => Utils.getPrimaryKeyCond(d, pks));
892
+ pk = data.map(d => Utils.getOrderedPrimaryKeys(d, meta));
894
893
  }
895
894
  else {
896
895
  res.row ??= {};
@@ -949,8 +948,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
949
948
  }
950
949
  res = await this.rethrow(qb.execute('run', false));
951
950
  }
952
- /* v8 ignore next */
953
- const pk = pks.map(pk => Utils.extractPK(data[pk] || where, meta));
951
+ const pk = Utils.getOrderedPrimaryKeys({ ...where, ...data }, meta);
954
952
  await this.processManyToMany(meta, pk, collections, true, options);
955
953
  return res;
956
954
  }
@@ -1072,7 +1070,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
1072
1070
  ? `(${pks.map(() => '?').join(', ')})`
1073
1071
  : `(${pks.map(pk => `${this.platform.quoteIdentifier(pk)} = ?`).join(' and ')})`;
1074
1072
  const conds = where.map(cond => {
1075
- if (Utils.isPlainObject(cond) && Utils.getObjectKeysSize(cond) === 1) {
1073
+ // with multiple PK columns the condition is looked up by property name, so it needs to stay an object
1074
+ if (pks.length === 1 && Utils.isPlainObject(cond) && Utils.getObjectKeysSize(cond) === 1) {
1076
1075
  cond = Object.values(cond)[0];
1077
1076
  }
1078
1077
  if (pks.length > 1) {
@@ -1100,7 +1099,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1100
1099
  sql += conds.join(' or ');
1101
1100
  }
1102
1101
  if (this.platform.usesReturningStatement() && returning.size > 0) {
1103
- const returningFields = Utils.flatten([...returning].map(prop => meta.properties[prop].fieldNames));
1102
+ const returningFields = Utils.flatten([...returning].map(prop => (meta.properties[prop] ?? meta.root.properties[prop]).fieldNames));
1104
1103
  /* v8 ignore next */
1105
1104
  sql +=
1106
1105
  returningFields.length > 0
@@ -1112,7 +1111,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
1112
1111
  }
1113
1112
  const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options)));
1114
1113
  for (let i = 0; i < collections.length; i++) {
1115
- await this.processManyToMany(meta, where[i], collections[i], false, options);
1114
+ const pk = Utils.getOrderedPrimaryKeys(where[i], meta);
1115
+ await this.processManyToMany(meta, pk, collections[i], false, options);
1116
1116
  }
1117
1117
  return res;
1118
1118
  }
@@ -1316,7 +1316,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1316
1316
  }
1317
1317
  }
1318
1318
  }
1319
- return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
1319
+ return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name, ownerMeta);
1320
1320
  }
1321
1321
  /**
1322
1322
  * Load from a polymorphic M:N pivot table.
@@ -1337,10 +1337,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
1337
1337
  async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
1338
1338
  const pivotMeta = this.metadata.get(prop.pivotEntity);
1339
1339
  const targetMeta = prop.targetMeta;
1340
- // Build condition: discriminator = 'post' AND {discriminator} IN (...)
1340
+ // `prop.discriminator` spans all owner FK columns but carries no target metadata, so a composite
1341
+ // owner PK neither expands to a tuple condition nor gets mapped back; the virtual M:1 relation
1342
+ // to this discriminator's owner describes the same columns as an actual relation
1343
+ const ownerMeta = this.metadata.get(pivotMeta.polymorphicDiscriminatorMap[prop.discriminatorValue]);
1344
+ const ownerProp = pivotMeta.properties[`${prop.discriminator}_${ownerMeta.tableName}`];
1345
+ // Build condition: discriminator = 'post' AND {owner} IN (...)
1341
1346
  const cond = {
1342
1347
  [prop.discriminatorColumn]: prop.discriminatorValue,
1343
- [prop.discriminator]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
1348
+ [ownerProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
1344
1349
  };
1345
1350
  if (!Utils.isEmpty(where)) {
1346
1351
  cond[inverseProp.name] = { ...where };
@@ -1351,9 +1356,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
1351
1356
  const childExclude = !Utils.isEmpty(options?.exclude)
1352
1357
  ? options.exclude.map(f => `${inverseProp.name}.${f}`)
1353
1358
  : [];
1359
+ // the owner relation is virtual, so its FK columns have to be selected via the pivot props that
1360
+ // cover them; only the first owner of a shared pivot gets the flat prop named after the
1361
+ // discriminator, and it keeps that owner's columns, so later owners get per-column props instead
1362
+ const ownerFields = Utils.unique(prop.joinColumns.map(col => (pivotMeta.properties[col] ? col : prop.discriminator)));
1354
1363
  const fields = pivotJoin
1355
- ? [inverseProp.name, prop.discriminator, prop.discriminatorColumn]
1356
- : [inverseProp.name, prop.discriminator, prop.discriminatorColumn, ...childFields];
1364
+ ? [inverseProp.name, ...ownerFields, prop.discriminatorColumn]
1365
+ : [inverseProp.name, ...ownerFields, prop.discriminatorColumn, ...childFields];
1357
1366
  const res = await this.find(pivotMeta.class, cond, {
1358
1367
  ctx,
1359
1368
  ...options,
@@ -1374,7 +1383,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1374
1383
  _populateWhere: 'infer',
1375
1384
  populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
1376
1385
  });
1377
- return this.buildPivotResultMap(owners, res, prop.discriminator, inverseProp.name);
1386
+ return this.buildPivotResultMap(owners, res, ownerProp.name, inverseProp.name, ownerMeta);
1378
1387
  }
1379
1388
  /**
1380
1389
  * Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
@@ -1423,7 +1432,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1423
1432
  _populateWhere: 'infer',
1424
1433
  populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
1425
1434
  });
1426
- return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
1435
+ return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName, tagProp.targetMeta);
1427
1436
  }
1428
1437
  /**
1429
1438
  * Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
@@ -1519,19 +1528,23 @@ export class AbstractSqlDriver extends DatabaseDriver {
1519
1528
  }
1520
1529
  }
1521
1530
  const result = orphanedRows.size > 0 ? pivotRows.filter(r => !orphanedRows.has(r)) : pivotRows;
1522
- return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator);
1531
+ return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator, ownerMeta);
1523
1532
  }
1524
1533
  /**
1525
1534
  * Build a map from owner PKs to their related entities from pivot table results.
1526
1535
  */
1527
- buildPivotResultMap(owners, results, keyProp, valueProp) {
1536
+ buildPivotResultMap(owners, results, keyProp, valueProp, ownerMeta) {
1528
1537
  const map = {};
1529
1538
  for (const owner of owners) {
1530
1539
  const key = Utils.getPrimaryKeyHash(owner);
1531
1540
  map[key] = [];
1532
1541
  }
1533
1542
  for (const item of results) {
1534
- const key = Utils.getPrimaryKeyHash(Utils.asArray(item[keyProp]));
1543
+ const fk = item[keyProp];
1544
+ // the owner PKs are always flat, while the pivot FK follows the owner PK structure,
1545
+ // so a PK built from a relation to another composite PK entity needs flattening too
1546
+ const pks = ownerMeta && fk != null ? Utils.getOrderedPrimaryKeys(fk, ownerMeta) : Utils.asArray(fk);
1547
+ const key = Utils.getPrimaryKeyHash(pks);
1535
1548
  const entity = item[valueProp];
1536
1549
  if (map[key]) {
1537
1550
  map[key].push(entity);
@@ -1657,8 +1670,23 @@ export class AbstractSqlDriver extends DatabaseDriver {
1657
1670
  const [propName, ref] = hint.field.split(':', 2);
1658
1671
  return { propName, ref, children: hint.children };
1659
1672
  });
1673
+ // with `fixedOrder` the pivot PK is the order column, which is not guaranteed to be unique when the
1674
+ // pivot table is managed externally, so we disambiguate the rows by their FKs on top of the PK
1675
+ // (including virtual ones, as the owner FK of a polymorphic pivot is only mapped via non-persisted relations)
1676
+ const pivotRelations = meta.pivotTable && !meta.compositePK ? meta.relations.filter(p => p.kind === ReferenceKind.MANY_TO_ONE) : [];
1660
1677
  for (const item of rawResults) {
1661
- const pk = Utils.getCompositeKeyHash(item, meta);
1678
+ // flat hash, so nested composite PK values keep their own separators and cannot collide
1679
+ let pk = Utils.getCompositeKeyHash(item, meta, false, undefined, true);
1680
+ if (pivotRelations.length > 0) {
1681
+ pk = Utils.getPrimaryKeyHash([
1682
+ pk,
1683
+ ...pivotRelations.flatMap(p => {
1684
+ const value = item[p.name];
1685
+ // composite FKs are mapped to an array of values, which `extractPK` does not accept
1686
+ return (Array.isArray(value) ? Utils.flatten(value, true) : Utils.extractPK(value, p.targetMeta));
1687
+ }),
1688
+ ]);
1689
+ }
1662
1690
  if (map[pk]) {
1663
1691
  for (const { propName } of hints) {
1664
1692
  if (!item[propName]) {
@@ -2087,7 +2115,20 @@ export class AbstractSqlDriver extends DatabaseDriver {
2087
2115
  const ret = {};
2088
2116
  for (const prop of meta.relations) {
2089
2117
  if (prop.kind === ReferenceKind.MANY_TO_MANY && data[prop.name]) {
2090
- ret[prop.name] = data[prop.name].map((item) => Utils.asArray(item));
2118
+ // union targets are validated to have a single PK column, so a pivot row is always keyed
2119
+ // by exactly `[discriminator, pk]` - anything else cannot address a target table
2120
+ const discriminators = QueryHelper.isUnionTargetPolymorphic(prop)
2121
+ ? Object.keys(prop.discriminatorMap)
2122
+ : undefined;
2123
+ ret[prop.name] = data[prop.name].map((item) => {
2124
+ const values = Utils.asArray(item);
2125
+ if (discriminators && !(values.length === 2 && discriminators.includes('' + values[0]))) {
2126
+ throw new Error(`Cannot resolve the discriminator value of ${meta.className}.${prop.name} from '${values.join(', ')}', ` +
2127
+ `as the same primary key can exist in any of the target tables. ` +
2128
+ `Pass the target as a [discriminator, ...primaryKey] tuple, e.g. ${JSON.stringify([discriminators[0], ...values])}.`);
2129
+ }
2130
+ return values;
2131
+ });
2091
2132
  delete data[prop.name];
2092
2133
  }
2093
2134
  }
@@ -2124,7 +2165,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
2124
2165
  if (hint.children) {
2125
2166
  const targetMeta = prop.targetMeta;
2126
2167
  if (targetMeta) {
2127
- const inner = this.buildPopulateWhere(targetMeta, hint.children, {});
2168
+ // only joined children contribute to the ON conditions, the rest is handled by the entity loader
2169
+ const children = this.joinedProps(targetMeta, hint.children, options);
2170
+ const inner = this.buildPopulateWhere(targetMeta, children, { strategy: options.strategy });
2128
2171
  if (!Utils.isEmpty(inner) || RawQueryFragment.hasObjectFragments(inner)) {
2129
2172
  where[prop.name] ??= {};
2130
2173
  Object.assign(where[prop.name], inner);
@@ -6,6 +6,7 @@ import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
6
6
  /** Base class for SQL database platforms, providing SQL generation and quoting utilities. */
7
7
  export declare abstract class AbstractSqlPlatform extends Platform {
8
8
  #private;
9
+ private static readonly ORDER_BY_DIRECTIONS;
9
10
  protected readonly schemaHelper?: SchemaHelper;
10
11
  usesPivotTable(): boolean;
11
12
  indexForeignKeys(): boolean;
@@ -46,6 +47,12 @@ export declare abstract class AbstractSqlPlatform extends Platform {
46
47
  * @internal
47
48
  */
48
49
  getOrderByExpression(column: string, direction: string, collation?: string): string[];
50
+ /**
51
+ * `toLowerCase()` folds every `QueryOrder` enum member (and the normalized `QueryOrderNumeric`
52
+ * values) onto the six allow-listed directions, so only unknown values are rejected.
53
+ * @internal
54
+ */
55
+ validateOrderByDirection(direction: string): string;
49
56
  /**
50
57
  * Quotes a collation name for use in COLLATE clauses.
51
58
  * @internal
@@ -5,6 +5,14 @@ import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
5
5
  /** Base class for SQL database platforms, providing SQL generation and quoting utilities. */
6
6
  export class AbstractSqlPlatform extends Platform {
7
7
  static #JSON_PROPERTY_NAME_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
8
+ static ORDER_BY_DIRECTIONS = new Set([
9
+ 'asc',
10
+ 'desc',
11
+ 'asc nulls first',
12
+ 'asc nulls last',
13
+ 'desc nulls first',
14
+ 'desc nulls last',
15
+ ]);
8
16
  schemaHelper;
9
17
  usesPivotTable() {
10
18
  return true;
@@ -115,10 +123,23 @@ export class AbstractSqlPlatform extends Platform {
115
123
  * @internal
116
124
  */
117
125
  getOrderByExpression(column, direction, collation) {
126
+ const dir = this.validateOrderByDirection(direction);
118
127
  if (collation) {
119
- return [`${column} collate ${this.quoteCollation(collation)} ${direction.toLowerCase()}`];
128
+ return [`${column} collate ${this.quoteCollation(collation)} ${dir}`];
120
129
  }
121
- return [`${column} ${direction.toLowerCase()}`];
130
+ return [`${column} ${dir}`];
131
+ }
132
+ /**
133
+ * `toLowerCase()` folds every `QueryOrder` enum member (and the normalized `QueryOrderNumeric`
134
+ * values) onto the six allow-listed directions, so only unknown values are rejected.
135
+ * @internal
136
+ */
137
+ validateOrderByDirection(direction) {
138
+ const dir = ('' + direction).toLowerCase().trim();
139
+ if (!AbstractSqlPlatform.ORDER_BY_DIRECTIONS.has(dir)) {
140
+ throw new Error(`Invalid order direction: '${direction}'`);
141
+ }
142
+ return dir;
122
143
  }
123
144
  /**
124
145
  * Quotes a collation name for use in COLLATE clauses.
@@ -60,14 +60,14 @@ export declare class SqlEntityManager<Driver extends AbstractSqlDriver = Abstrac
60
60
  * `signal` / `inflightQueryAbortStrategy` (set via `em.fork({ signal })`) is applied automatically.
61
61
  * For per-call cancellation use the options-bag overload below.
62
62
  */
63
- execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', loggerContext?: LoggingOptions): Promise<T>;
63
+ execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[] | Dictionary, method?: 'all' | 'get' | 'run', loggerContext?: LoggingOptions): Promise<T>;
64
64
  /**
65
65
  * Executes a raw SQL query with an options bag carrying `method`, `loggerContext`, `signal`
66
66
  * and `inflightQueryAbortStrategy`. Per-call `signal` / `inflightQueryAbortStrategy` override
67
67
  * the fork-level defaults set via `em.fork({ signal })`. The current transaction context is
68
68
  * applied automatically.
69
69
  */
70
- execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params: any[], options: EmExecuteOptions): Promise<T>;
70
+ execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params: any[] | Dictionary, options: EmExecuteOptions): Promise<T>;
71
71
  /**
72
72
  * @inheritDoc
73
73
  */
@@ -75,8 +75,7 @@ export class SqlEntityManager extends EntityManager {
75
75
  */
76
76
  async countBy(entityName, groupBy, options = {}) {
77
77
  const em = this.getContext(false);
78
- options = { ...options };
79
- em.prepareOptions(options);
78
+ options = em.prepareOptions(options);
80
79
  const meta = em.getMetadata().find(entityName);
81
80
  const fields = Utils.asArray(groupBy);
82
81
  const { where: rawWhere, ...countOptions } = options;
@@ -168,7 +168,7 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
168
168
  const fields = this.options.groupBy.map(field => this.quote(field));
169
169
  this.parts.push(`group by ${fields.join(', ')}`);
170
170
  }
171
- if (this.options.having) {
171
+ if (this.options.having?.sql.trim()) {
172
172
  this.parts.push(`having ${this.options.having.sql}`);
173
173
  this.params.push(...this.options.having.params);
174
174
  }
@@ -114,7 +114,7 @@ export class BaseMySqlPlatform extends AbstractSqlPlatform {
114
114
  }
115
115
  getOrderByExpression(column, direction, collation) {
116
116
  const ret = [];
117
- const dir = direction.toLowerCase();
117
+ const dir = this.validateOrderByDirection(direction);
118
118
  const col = collation ? `${column} collate ${this.quoteCollation(collation)}` : column;
119
119
  if (dir in this.ORDER_BY_NULLS_TRANSLATE) {
120
120
  ret.push(`${col} ${this.ORDER_BY_NULLS_TRANSLATE[dir]}`);
@@ -53,6 +53,7 @@ export declare class MySqlSchemaHelper extends SchemaHelper {
53
53
  getPreAlterTable(tableDiff: TableDifference, safe: boolean): string[];
54
54
  getRenameColumnSQL(tableName: string, oldColumnName: string, to: Column): string;
55
55
  getRenameIndexSQL(tableName: string, index: IndexDef, oldIndexName: string): string[];
56
+ protected hasInlineColumnComment(): boolean;
56
57
  getChangeColumnCommentSQL(tableName: string, to: Column, schemaName?: string): string;
57
58
  alterTableColumn(column: Column, table: DatabaseTable, changedProperties: Set<string>): string[];
58
59
  private getColumnDeclarationSQL;
@@ -54,10 +54,10 @@ export class MySqlSchemaHelper extends SchemaHelper {
54
54
  return sql;
55
55
  }
56
56
  getListTablesSQL() {
57
- return `select table_name as table_name, nullif(table_schema, schema()) as schema_name, table_comment as table_comment, table_collation as table_collation from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema()`;
57
+ return `select table_name as table_name, nullif(table_schema, schema()) as schema_name, table_comment as table_comment, table_collation as table_collation from information_schema.tables where table_type = 'BASE TABLE' and table_schema = schema() order by table_name`;
58
58
  }
59
59
  getListViewsSQL() {
60
- return `select table_name as view_name, nullif(table_schema, schema()) as schema_name, view_definition from information_schema.views where table_schema = schema()`;
60
+ return `select table_name as view_name, nullif(table_schema, schema()) as schema_name, view_definition from information_schema.views where table_schema = schema() order by table_name`;
61
61
  }
62
62
  async loadViews(schema, connection, schemaName, ctx) {
63
63
  const views = await connection.execute(this.getListViewsSQL(), [], 'all', ctx);
@@ -322,7 +322,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
322
322
  const ret = [];
323
323
  for (const event of trigger.events) {
324
324
  const name = trigger.events.length > 1 ? `${trigger.name}_${event}` : trigger.name;
325
- ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ROW begin ${trigger.body}; end`);
325
+ ret.push(`create trigger ${this.quote(name)} ${timing} ${event.toUpperCase()} on ${table.getQuotedName()} for each ROW begin ${this.normalizeTriggerBody(trigger.body)} end`);
326
326
  }
327
327
  return ret.join(';\n');
328
328
  }
@@ -372,6 +372,7 @@ export class MySqlSchemaHelper extends SchemaHelper {
372
372
  from information_schema.routines r
373
373
  where r.routine_schema = database()
374
374
  and r.routine_type in ('PROCEDURE', 'FUNCTION')
375
+ order by r.routine_name
375
376
  `;
376
377
  const [rows, params] = await Promise.all([
377
378
  connection.execute(sql),
@@ -539,6 +540,9 @@ export class MySqlSchemaHelper extends SchemaHelper {
539
540
  const keyName = this.quote(index.keyName);
540
541
  return [`alter table ${tableName} rename index ${oldIndexName} to ${keyName}`];
541
542
  }
543
+ hasInlineColumnComment() {
544
+ return true;
545
+ }
542
546
  getChangeColumnCommentSQL(tableName, to, schemaName) {
543
547
  tableName = this.quote(tableName);
544
548
  const columnName = this.quote(to.name);
@@ -234,7 +234,7 @@ export class OracleNativeQueryBuilder extends NativeQueryBuilder {
234
234
  const fields = this.options.groupBy.map(field => this.quote(field));
235
235
  this.parts.push(`group by ${fields.join(', ')}`);
236
236
  }
237
- if (this.options.having) {
237
+ if (this.options.having?.sql.trim()) {
238
238
  this.parts.push(`having ${this.options.having.sql}`);
239
239
  this.params.push(...this.options.having.params);
240
240
  }
@@ -69,6 +69,8 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
69
69
  createTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
70
70
  /** Generates SQL to drop a PostgreSQL trigger and its associated function. */
71
71
  dropTrigger(table: DatabaseTable, trigger: SqlTriggerDef): string;
72
+ /** Flattens `;\n` inside the dollar-quoted blocks of a raw DDL expression, which are not statement boundaries. */
73
+ private flattenDollarQuotedBodies;
72
74
  createRoutine(routine: SqlRoutineDef): string;
73
75
  dropRoutine(routine: SqlRoutineDef): string;
74
76
  getAllRoutines(connection: AbstractSqlConnection, schemas?: string[]): Promise<SqlRoutineDef[]>;
@@ -131,6 +133,8 @@ export declare class PostgreSqlSchemaHelper extends SchemaHelper {
131
133
  * Build the column list for a PostgreSQL index.
132
134
  */
133
135
  protected getIndexColumns(index: IndexDef): string;
136
+ /** Non-default index access methods (gin, gist, brin, hash, ...), normalized to lower case. */
137
+ getIndexAccessMethod(index: IndexDef): string;
134
138
  /**
135
139
  * PostgreSQL-specific index options like fill factor.
136
140
  */