@mikro-orm/sql 7.0.0-dev.99 → 7.0.0-rc.1

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.
Files changed (64) hide show
  1. package/AbstractSqlConnection.d.ts +2 -4
  2. package/AbstractSqlConnection.js +3 -7
  3. package/AbstractSqlDriver.d.ts +89 -23
  4. package/AbstractSqlDriver.js +630 -197
  5. package/AbstractSqlPlatform.d.ts +11 -5
  6. package/AbstractSqlPlatform.js +18 -5
  7. package/PivotCollectionPersister.d.ts +5 -0
  8. package/PivotCollectionPersister.js +30 -12
  9. package/SqlEntityManager.d.ts +2 -2
  10. package/dialects/mysql/{MySqlPlatform.d.ts → BaseMySqlPlatform.d.ts} +4 -3
  11. package/dialects/mysql/{MySqlPlatform.js → BaseMySqlPlatform.js} +9 -4
  12. package/dialects/mysql/MySqlSchemaHelper.d.ts +12 -1
  13. package/dialects/mysql/MySqlSchemaHelper.js +97 -6
  14. package/dialects/mysql/index.d.ts +1 -2
  15. package/dialects/mysql/index.js +1 -2
  16. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +106 -0
  17. package/dialects/postgresql/BasePostgreSqlPlatform.js +350 -0
  18. package/dialects/postgresql/FullTextType.d.ts +14 -0
  19. package/dialects/postgresql/FullTextType.js +59 -0
  20. package/dialects/postgresql/PostgreSqlExceptionConverter.d.ts +8 -0
  21. package/dialects/postgresql/PostgreSqlExceptionConverter.js +47 -0
  22. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +90 -0
  23. package/dialects/postgresql/PostgreSqlSchemaHelper.js +732 -0
  24. package/dialects/postgresql/index.d.ts +3 -0
  25. package/dialects/postgresql/index.js +3 -0
  26. package/dialects/sqlite/BaseSqliteConnection.d.ts +1 -0
  27. package/dialects/sqlite/BaseSqliteConnection.js +13 -0
  28. package/dialects/sqlite/BaseSqlitePlatform.d.ts +6 -0
  29. package/dialects/sqlite/BaseSqlitePlatform.js +12 -0
  30. package/dialects/sqlite/SqliteSchemaHelper.d.ts +25 -0
  31. package/dialects/sqlite/SqliteSchemaHelper.js +145 -19
  32. package/dialects/sqlite/index.d.ts +0 -1
  33. package/dialects/sqlite/index.js +0 -1
  34. package/package.json +5 -6
  35. package/plugin/transformer.d.ts +1 -1
  36. package/plugin/transformer.js +1 -1
  37. package/query/CriteriaNode.d.ts +9 -5
  38. package/query/CriteriaNode.js +16 -15
  39. package/query/CriteriaNodeFactory.d.ts +6 -6
  40. package/query/CriteriaNodeFactory.js +33 -31
  41. package/query/NativeQueryBuilder.d.ts +3 -2
  42. package/query/NativeQueryBuilder.js +1 -2
  43. package/query/ObjectCriteriaNode.js +51 -36
  44. package/query/QueryBuilder.d.ts +569 -79
  45. package/query/QueryBuilder.js +614 -171
  46. package/query/QueryBuilderHelper.d.ts +24 -16
  47. package/query/QueryBuilderHelper.js +167 -78
  48. package/query/ScalarCriteriaNode.js +2 -2
  49. package/query/raw.d.ts +11 -3
  50. package/query/raw.js +1 -2
  51. package/schema/DatabaseSchema.d.ts +15 -2
  52. package/schema/DatabaseSchema.js +143 -15
  53. package/schema/DatabaseTable.d.ts +12 -0
  54. package/schema/DatabaseTable.js +91 -31
  55. package/schema/SchemaComparator.d.ts +8 -0
  56. package/schema/SchemaComparator.js +127 -3
  57. package/schema/SchemaHelper.d.ts +26 -3
  58. package/schema/SchemaHelper.js +98 -11
  59. package/schema/SqlSchemaGenerator.d.ts +10 -0
  60. package/schema/SqlSchemaGenerator.js +137 -9
  61. package/tsconfig.build.tsbuildinfo +1 -0
  62. package/typings.d.ts +78 -38
  63. package/dialects/postgresql/PostgreSqlTableCompiler.d.ts +0 -1
  64. package/dialects/postgresql/PostgreSqlTableCompiler.js +0 -1
@@ -1,4 +1,4 @@
1
- import { ALIAS_REPLACEMENT_RE, DatabaseDriver, EntityManagerType, getLoadingStrategy, getOnConflictFields, getOnConflictReturningFields, helper, isRaw, LoadStrategy, parseJsonSafe, QueryFlag, QueryHelper, QueryOrder, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
1
+ import { ALIAS_REPLACEMENT_RE, DatabaseDriver, EntityManagerType, getLoadingStrategy, getOnConflictFields, getOnConflictReturningFields, helper, isRaw, LoadStrategy, parseJsonSafe, PolymorphicRef, QueryFlag, QueryHelper, QueryOrder, raw, RawQueryFragment, ReferenceKind, Utils, } from '@mikro-orm/core';
2
2
  import { QueryBuilder } from './query/QueryBuilder.js';
3
3
  import { JoinType, QueryType } from './query/enums.js';
4
4
  import { SqlEntityManager } from './SqlEntityManager.js';
@@ -17,6 +17,29 @@ export class AbstractSqlDriver extends DatabaseDriver {
17
17
  getPlatform() {
18
18
  return this.platform;
19
19
  }
20
+ /** Evaluates a formula callback, handling both string and Raw return values. */
21
+ evaluateFormula(formula, columns, table) {
22
+ const result = formula(columns, table);
23
+ return isRaw(result) ? this.platform.formatQuery(result.sql, result.params) : result;
24
+ }
25
+ /** For TPT entities, returns ownProps (columns in this table); otherwise returns all props. */
26
+ getTableProps(meta) {
27
+ return meta.inheritanceType === 'tpt' && meta.ownProps ? meta.ownProps : meta.props;
28
+ }
29
+ /** Creates a FormulaTable object for use in formula callbacks. */
30
+ createFormulaTable(alias, meta, schema) {
31
+ const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
32
+ const qualifiedName = effectiveSchema ? `${effectiveSchema}.${meta.tableName}` : meta.tableName;
33
+ return { alias, name: meta.tableName, schema: effectiveSchema, qualifiedName, toString: () => alias };
34
+ }
35
+ validateSqlOptions(options) {
36
+ if (options.collation != null && typeof options.collation !== 'string') {
37
+ throw new Error('Collation option for SQL drivers must be a string (collation name). Use a CollationOptions object only with MongoDB.');
38
+ }
39
+ if (options.indexHint != null && typeof options.indexHint !== 'string') {
40
+ throw new Error('indexHint for SQL drivers must be a string (e.g. \'force index(my_index)\'). Use an object only with MongoDB.');
41
+ }
42
+ }
20
43
  createEntityManager(useContext) {
21
44
  const EntityManagerClass = this.config.get('entityManager', SqlEntityManager);
22
45
  return new EntityManagerClass(this.config, this, this.metadata, useContext);
@@ -25,14 +48,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
25
48
  const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
26
49
  const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
27
50
  const joinedProps = this.joinedProps(meta, populate, options);
28
- const qb = this.createQueryBuilder(meta.className, options.ctx, connectionType, false, options.logging, undefined, options.em);
29
- const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options);
51
+ const schema = this.getSchemaName(meta, options);
52
+ const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em).withSchema(schema);
53
+ const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
30
54
  const orderBy = this.buildOrderBy(qb, meta, populate, options);
31
55
  const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
32
56
  Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
33
57
  if (Utils.isPrimaryKey(where, meta.compositePK)) {
34
58
  where = { [Utils.getPrimaryKeyHash(meta.primaryKeys)]: where };
35
59
  }
60
+ this.validateSqlOptions(options);
36
61
  const { first, last, before, after } = options;
37
62
  const isCursorPagination = [first, last, before, after].some(v => v != null);
38
63
  qb.__populateWhere = options._populateWhere;
@@ -43,9 +68,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
43
68
  .groupBy(options.groupBy)
44
69
  .having(options.having)
45
70
  .indexHint(options.indexHint)
71
+ .collation(options.collation)
46
72
  .comment(options.comments)
47
- .hintComment(options.hintComments)
48
- .withSchema(this.getSchemaName(meta, options));
73
+ .hintComment(options.hintComments);
49
74
  if (isCursorPagination) {
50
75
  const { orderBy: newOrderBy, where } = this.processCursorOptions(meta, options, orderBy);
51
76
  qb.andWhere(where).orderBy(newOrderBy);
@@ -66,8 +91,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
66
91
  }
67
92
  async find(entityName, where, options = {}) {
68
93
  options = { populate: [], orderBy: [], ...options };
69
- const meta = this.metadata.find(entityName);
70
- if (meta?.virtual) {
94
+ const meta = this.metadata.get(entityName);
95
+ if (meta.virtual) {
71
96
  return this.findVirtual(entityName, where, options);
72
97
  }
73
98
  const qb = await this.createQueryBuilderFromOptions(meta, where, options);
@@ -165,16 +190,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
165
190
  yield* res;
166
191
  }
167
192
  async wrapVirtualExpressionInSubquery(meta, expression, where, options, type) {
168
- const qb = await this.createQueryBuilderFromOptions(meta, where, options);
193
+ const qb = await this.createQueryBuilderFromOptions(meta, where, this.forceBalancedStrategy(options));
169
194
  qb.setFlag(QueryFlag.DISABLE_PAGINATE);
170
195
  const isCursorPagination = [options.first, options.last, options.before, options.after].some(v => v != null);
171
196
  const native = qb.getNativeQuery(false);
172
197
  if (type === QueryType.COUNT) {
173
- native
174
- .clear('select')
175
- .clear('limit')
176
- .clear('offset')
177
- .count();
198
+ native.clear('select').clear('limit').clear('offset').count();
178
199
  }
179
200
  native.from(raw(`(${expression}) as ${this.platform.quoteIdentifier(qb.alias)}`));
180
201
  const query = native.compile();
@@ -188,7 +209,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
188
209
  return res.map(row => this.mapResult(row, meta));
189
210
  }
190
211
  async *wrapVirtualExpressionInSubqueryStream(meta, expression, where, options, type) {
191
- const qb = await this.createQueryBuilderFromOptions(meta, where, options);
212
+ const qb = await this.createQueryBuilderFromOptions(meta, where, this.forceBalancedStrategy(options));
192
213
  qb.unsetFlag(QueryFlag.DISABLE_PAGINATE);
193
214
  const native = qb.getNativeQuery(false);
194
215
  native.from(raw(`(${expression}) as ${this.platform.quoteIdentifier(qb.alias)}`));
@@ -199,7 +220,34 @@ export class AbstractSqlDriver extends DatabaseDriver {
199
220
  yield this.mapResult(row, meta);
200
221
  }
201
222
  }
223
+ /**
224
+ * Virtual entities have no PKs, so to-many populate joins can't be deduplicated.
225
+ * Force balanced strategy to load to-many relations via separate queries.
226
+ */
227
+ forceBalancedStrategy(options) {
228
+ const clearStrategy = (hints) => {
229
+ return hints.map(hint => ({
230
+ ...hint,
231
+ strategy: undefined,
232
+ children: hint.children ? clearStrategy(hint.children) : undefined,
233
+ }));
234
+ };
235
+ const opts = { ...options, strategy: 'balanced' };
236
+ if (Array.isArray(opts.populate)) {
237
+ opts.populate = clearStrategy(opts.populate);
238
+ }
239
+ return opts;
240
+ }
202
241
  mapResult(result, meta, populate = [], qb, map = {}) {
242
+ // For TPT inheritance, map aliased parent table columns back to their field names
243
+ if (qb && meta.inheritanceType === 'tpt' && meta.tptParent) {
244
+ this.mapTPTColumns(result, meta, qb);
245
+ }
246
+ // For TPT polymorphic queries (querying a base class), map child table fields
247
+ if (qb && meta.inheritanceType === 'tpt' && meta.allTPTDescendants?.length) {
248
+ const mainAlias = qb.mainAlias?.aliasName ?? 'e0';
249
+ this.mapTPTChildFields(result, meta, mainAlias, qb, result);
250
+ }
203
251
  const ret = super.mapResult(result, meta);
204
252
  /* v8 ignore next */
205
253
  if (!ret) {
@@ -211,6 +259,33 @@ export class AbstractSqlDriver extends DatabaseDriver {
211
259
  }
212
260
  return ret;
213
261
  }
262
+ /**
263
+ * Maps aliased columns from TPT parent tables back to their original field names.
264
+ * TPT parent columns are selected with aliases like `parent_alias__column_name`,
265
+ * and need to be renamed back to `column_name` for the result mapper to work.
266
+ */
267
+ mapTPTColumns(result, meta, qb) {
268
+ const tptAliases = qb._tptAlias;
269
+ // Walk up the TPT hierarchy
270
+ let parentMeta = meta.tptParent;
271
+ while (parentMeta) {
272
+ const parentAlias = tptAliases[parentMeta.className];
273
+ if (parentAlias) {
274
+ // Rename columns from this parent table
275
+ for (const prop of parentMeta.ownProps) {
276
+ for (const fieldName of prop.fieldNames) {
277
+ const aliasedKey = `${parentAlias}__${fieldName}`;
278
+ if (aliasedKey in result) {
279
+ // Copy the value to the unaliased field name and remove the aliased key
280
+ result[fieldName] = result[aliasedKey];
281
+ delete result[aliasedKey];
282
+ }
283
+ }
284
+ }
285
+ }
286
+ parentMeta = parentMeta.tptParent;
287
+ }
288
+ }
214
289
  mapJoinedProps(result, meta, populate, qb, root, map, parentJoinPath) {
215
290
  const joinedProps = this.joinedProps(meta, populate);
216
291
  joinedProps.forEach(hint => {
@@ -220,8 +295,45 @@ export class AbstractSqlDriver extends DatabaseDriver {
220
295
  if (!prop) {
221
296
  return;
222
297
  }
298
+ // Polymorphic to-one: iterate targets, find the matching one, build entity from its columns.
299
+ // Skip :ref hints — no JOINs were created, so the FK reference is already set by the result mapper.
300
+ if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
301
+ const basePath = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
302
+ const pathPrefix = !parentJoinPath ? '[populate]' : '';
303
+ let matched = false;
304
+ for (const targetMeta of prop.polymorphTargets) {
305
+ const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
306
+ const relationAlias = qb.getAliasForJoinPath(targetPath, { matchPopulateJoins: true });
307
+ const meta2 = targetMeta;
308
+ const targetProps = meta2.props.filter(p => this.platform.shouldHaveColumn(p, hint.children || []));
309
+ const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => root[`${relationAlias}__${name}`] != null));
310
+ if (hasPK && !matched) {
311
+ matched = true;
312
+ let relationPojo = {};
313
+ const tz = this.platform.getTimezone();
314
+ for (const p of targetProps) {
315
+ this.mapJoinedProp(relationPojo, p, relationAlias, root, tz, meta2);
316
+ }
317
+ // Inject the entity class constructor so that the factory creates the correct type
318
+ Object.defineProperty(relationPojo, 'constructor', { value: meta2.class, enumerable: false, configurable: true });
319
+ result[prop.name] = relationPojo;
320
+ const populateChildren = hint.children || [];
321
+ this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, targetPath);
322
+ }
323
+ // Clean up aliased columns for ALL targets (even non-matching ones)
324
+ for (const p of targetProps) {
325
+ for (const name of p.fieldNames) {
326
+ delete root[`${relationAlias}__${name}`];
327
+ }
328
+ }
329
+ }
330
+ if (!matched) {
331
+ result[prop.name] = null;
332
+ }
333
+ return;
334
+ }
223
335
  const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
224
- const meta2 = this.metadata.find(prop.type);
336
+ const meta2 = prop.targetMeta;
225
337
  let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
226
338
  if (!parentJoinPath) {
227
339
  path = '[populate]' + path;
@@ -237,7 +349,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
237
349
  // pivot ref joins via joined strategy need to be handled separately here, as they dont join the target entity
238
350
  if (pivotRefJoin) {
239
351
  let item;
240
- if (prop.inverseJoinColumns.length > 1) { // composite keys
352
+ if (prop.inverseJoinColumns.length > 1) {
353
+ // composite keys
241
354
  item = prop.inverseJoinColumns.map(name => root[`${relationAlias}__${name}`]);
242
355
  }
243
356
  else {
@@ -252,10 +365,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
252
365
  }
253
366
  return;
254
367
  }
255
- const mapToPk = !!(ref || prop.mapToPk);
256
- const targetProps = mapToPk
257
- ? meta2.getPrimaryProps()
258
- : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
368
+ const mapToPk = !hint.dataOnly && !!(ref || prop.mapToPk);
369
+ const targetProps = mapToPk ? meta2.getPrimaryProps() : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
259
370
  // If the primary key value for the relation is null, we know we haven't joined to anything
260
371
  // and therefore we don't return any record (since all values would be null)
261
372
  const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => {
@@ -280,7 +391,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
280
391
  .filter(prop => !ref && prop.persist === false && prop.fieldNames)
281
392
  .forEach(prop => {
282
393
  /* v8 ignore next */
283
- if (prop.fieldNames.length > 1) { // composite keys
394
+ if (prop.fieldNames.length > 1) {
395
+ // composite keys
284
396
  relationPojo[prop.name] = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
285
397
  }
286
398
  else {
@@ -290,41 +402,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
290
402
  });
291
403
  const tz = this.platform.getTimezone();
292
404
  for (const prop of targetProps) {
293
- if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
294
- continue;
295
- }
296
- if (prop.fieldNames.length > 1) { // composite keys
297
- const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
298
- const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
299
- relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
300
- }
301
- else if (prop.runtimeType === 'Date') {
302
- const alias = `${relationAlias}__${prop.fieldNames[0]}`;
303
- const value = root[alias];
304
- if (tz && tz !== 'local' && typeof value === 'string' && !value.includes('+') && value.lastIndexOf('-') < 11 && !value.endsWith('Z')) {
305
- relationPojo[prop.name] = this.platform.parseDate(value + tz);
306
- }
307
- else if (['string', 'number'].includes(typeof value)) {
308
- relationPojo[prop.name] = this.platform.parseDate(value);
309
- }
310
- else {
311
- relationPojo[prop.name] = value;
312
- }
313
- }
314
- else {
315
- const alias = `${relationAlias}__${prop.fieldNames[0]}`;
316
- relationPojo[prop.name] = root[alias];
317
- if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
318
- const item = parseJsonSafe(relationPojo[prop.name]);
319
- if (Array.isArray(item)) {
320
- relationPojo[prop.name] = item.map(row => row == null ? row : this.comparator.mapResult(prop.type, row));
321
- }
322
- else {
323
- relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.type, item);
324
- }
325
- }
326
- }
405
+ this.mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta2);
327
406
  }
407
+ // Handle TPT polymorphic child fields - map fields from child table aliases
408
+ this.mapTPTChildFields(relationPojo, meta2, relationAlias, qb, root);
328
409
  // properties can be mapped to multiple places, e.g. when sharing a column in multiple FKs,
329
410
  // so we need to delete them after everything is mapped from given level
330
411
  for (const prop of targetProps) {
@@ -348,6 +429,65 @@ export class AbstractSqlDriver extends DatabaseDriver {
348
429
  this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
349
430
  });
350
431
  }
432
+ /**
433
+ * Maps a single property from a joined result row into the relation pojo.
434
+ * Handles polymorphic FKs, composite keys, Date parsing, and embedded objects.
435
+ */
436
+ mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta, options) {
437
+ if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
438
+ return;
439
+ }
440
+ if (prop.polymorphic) {
441
+ const discriminatorAlias = `${relationAlias}__${prop.fieldNames[0]}`;
442
+ const discriminatorValue = root[discriminatorAlias];
443
+ const pkFieldNames = prop.fieldNames.slice(1);
444
+ const pkValues = pkFieldNames.map(name => root[`${relationAlias}__${name}`]);
445
+ const pkValue = pkValues.length === 1 ? pkValues[0] : pkValues;
446
+ if (discriminatorValue != null && pkValue != null) {
447
+ relationPojo[prop.name] = new PolymorphicRef(discriminatorValue, pkValue);
448
+ }
449
+ else {
450
+ relationPojo[prop.name] = null;
451
+ }
452
+ }
453
+ else if (prop.fieldNames.length > 1) {
454
+ // composite keys
455
+ const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
456
+ const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
457
+ relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
458
+ }
459
+ else if (prop.runtimeType === 'Date') {
460
+ const alias = `${relationAlias}__${prop.fieldNames[0]}`;
461
+ const value = root[alias];
462
+ if (tz && tz !== 'local' && typeof value === 'string' && !value.includes('+') && value.lastIndexOf('-') < 11 && !value.endsWith('Z')) {
463
+ relationPojo[prop.name] = this.platform.parseDate(value + tz);
464
+ }
465
+ else if (['string', 'number'].includes(typeof value)) {
466
+ relationPojo[prop.name] = this.platform.parseDate(value);
467
+ }
468
+ else {
469
+ relationPojo[prop.name] = value;
470
+ }
471
+ }
472
+ else {
473
+ const alias = `${relationAlias}__${prop.fieldNames[0]}`;
474
+ relationPojo[prop.name] = root[alias];
475
+ if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
476
+ const item = parseJsonSafe(relationPojo[prop.name]);
477
+ if (Array.isArray(item)) {
478
+ relationPojo[prop.name] = item.map(row => (row == null ? row : this.comparator.mapResult(prop.targetMeta, row)));
479
+ }
480
+ else {
481
+ relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
482
+ }
483
+ }
484
+ }
485
+ if (options?.deleteFromRoot) {
486
+ for (const name of prop.fieldNames) {
487
+ delete root[`${relationAlias}__${name}`];
488
+ }
489
+ }
490
+ }
351
491
  async count(entityName, where, options = {}) {
352
492
  const meta = this.metadata.get(entityName);
353
493
  if (meta.virtual) {
@@ -356,19 +496,22 @@ export class AbstractSqlDriver extends DatabaseDriver {
356
496
  options = { populate: [], ...options };
357
497
  const populate = options.populate;
358
498
  const joinedProps = this.joinedProps(meta, populate, options);
499
+ const schema = this.getSchemaName(meta, options);
359
500
  const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
360
501
  const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
361
502
  if (meta && !Utils.isEmpty(populate)) {
362
- this.buildFields(meta, populate, joinedProps, qb, qb.alias, options);
503
+ this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
363
504
  }
505
+ this.validateSqlOptions(options);
364
506
  qb.__populateWhere = options._populateWhere;
365
507
  qb.indexHint(options.indexHint)
508
+ .collation(options.collation)
366
509
  .comment(options.comments)
367
510
  .hintComment(options.hintComments)
368
511
  .groupBy(options.groupBy)
369
512
  .having(options.having)
370
513
  .populate(populate, joinedProps.length > 0 ? populateWhere : undefined, joinedProps.length > 0 ? options.populateFilter : undefined)
371
- .withSchema(this.getSchemaName(meta, options))
514
+ .withSchema(schema)
372
515
  .where(where);
373
516
  if (options.em) {
374
517
  await qb.applyJoinedFilters(options.em, options.filters);
@@ -377,19 +520,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
377
520
  }
378
521
  async nativeInsert(entityName, data, options = {}) {
379
522
  options.convertCustomTypes ??= true;
380
- const meta = this.metadata.find(entityName);
381
- const collections = this.extractManyToMany(entityName, data);
382
- const pks = meta?.primaryKeys ?? [this.config.getNamingStrategy().referenceColumnName()];
523
+ const meta = this.metadata.get(entityName);
524
+ const collections = this.extractManyToMany(meta, data);
383
525
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
384
526
  const res = await this.rethrow(qb.insert(data).execute('run', false));
385
527
  res.row = res.row || {};
386
528
  let pk;
387
- if (pks.length > 1) { // owner has composite pk
388
- pk = Utils.getPrimaryKeyCond(data, pks);
529
+ if (meta.primaryKeys.length > 1) {
530
+ // owner has composite pk
531
+ pk = Utils.getPrimaryKeyCond(data, meta.primaryKeys);
389
532
  }
390
533
  else {
391
534
  /* v8 ignore next */
392
- res.insertId = data[pks[0]] ?? res.insertId ?? res.row[pks[0]];
535
+ res.insertId = data[meta.primaryKeys[0]] ?? res.insertId ?? res.row[meta.primaryKeys[0]];
393
536
  pk = [res.insertId];
394
537
  }
395
538
  await this.processManyToMany(meta, pk, collections, false, options);
@@ -398,25 +541,26 @@ export class AbstractSqlDriver extends DatabaseDriver {
398
541
  async nativeInsertMany(entityName, data, options = {}, transform) {
399
542
  options.processCollections ??= true;
400
543
  options.convertCustomTypes ??= true;
401
- const meta = this.metadata.find(entityName)?.root;
402
- const collections = options.processCollections ? data.map(d => this.extractManyToMany(entityName, d)) : [];
403
- const pks = this.getPrimaryKeyFields(entityName);
544
+ const entityMeta = this.metadata.get(entityName);
545
+ const meta = entityMeta.inheritanceType === 'tpt' ? entityMeta : entityMeta.root;
546
+ const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
547
+ const pks = this.getPrimaryKeyFields(meta);
404
548
  const set = new Set();
405
549
  data.forEach(row => Utils.keys(row).forEach(k => set.add(k)));
406
- const props = [...set].map(name => meta?.properties[name] ?? { name, fieldNames: [name] });
407
- let fields = Utils.flatten(props.map(prop => prop.fieldNames));
550
+ const props = [...set].map(name => meta.properties[name] ?? { name, fieldNames: [name] });
551
+ // For STI with conflicting fieldNames, include all alternative columns
552
+ let fields = Utils.flatten(props.map(prop => prop.stiFieldNames ?? prop.fieldNames));
408
553
  const duplicates = Utils.findDuplicates(fields);
409
554
  const params = [];
410
555
  if (duplicates.length) {
411
556
  fields = Utils.unique(fields);
412
557
  }
413
- /* v8 ignore next */
414
- const tableName = meta ? this.getTableName(meta, options) : this.platform.quoteIdentifier(entityName);
558
+ const tableName = this.getTableName(meta, options);
415
559
  let sql = `insert into ${tableName} `;
416
560
  sql += fields.length > 0 ? '(' + fields.map(k => this.platform.quoteIdentifier(k)).join(', ') + ')' : `(${this.platform.quoteIdentifier(pks[0])})`;
417
- if (meta && this.platform.usesOutputStatement()) {
418
- const returningProps = meta.props
419
- .filter(prop => prop.persist !== false && prop.defaultRaw || prop.autoincrement || prop.generated)
561
+ if (this.platform.usesOutputStatement()) {
562
+ const returningProps = this.getTableProps(meta)
563
+ .filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
420
564
  .filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
421
565
  const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
422
566
  sql += returningFields.length > 0 ? ` output ${returningFields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}` : '';
@@ -457,25 +601,44 @@ export class AbstractSqlDriver extends DatabaseDriver {
457
601
  params.push(value);
458
602
  };
459
603
  if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
460
- sql += data.map(row => {
604
+ sql += data
605
+ .map(row => {
461
606
  const keys = [];
462
607
  const usedDups = [];
463
608
  props.forEach(prop => {
609
+ // For STI with conflicting fieldNames, use discriminator to determine which field gets value
610
+ if (prop.stiFieldNames && prop.stiFieldNameMap && meta.discriminatorColumn) {
611
+ const activeField = prop.stiFieldNameMap[row[meta.discriminatorColumn]];
612
+ for (const field of prop.stiFieldNames) {
613
+ params.push(field === activeField ? row[prop.name] : null);
614
+ keys.push('?');
615
+ }
616
+ return;
617
+ }
464
618
  if (prop.fieldNames.length > 1) {
465
619
  const newFields = [];
466
- const allParam = [...(Utils.asArray(row[prop.name]) ?? prop.fieldNames.map(() => null))];
620
+ let rawParam;
621
+ const target = row[prop.name];
622
+ if (prop.polymorphic && target instanceof PolymorphicRef) {
623
+ rawParam = target.toTuple();
624
+ }
625
+ else {
626
+ rawParam = Utils.asArray(target) ?? prop.fieldNames.map(() => null);
627
+ }
628
+ // Deep flatten nested arrays when needed (for deeply nested composite keys like Tag -> Comment -> Post -> User)
629
+ const needsFlatten = rawParam.length !== prop.fieldNames.length && rawParam.some(v => Array.isArray(v));
630
+ const allParam = needsFlatten ? Utils.flatten(rawParam, true) : rawParam;
467
631
  // TODO(v7): instead of making this conditional here, the entity snapshot should respect `ownColumns`,
468
632
  // but that means changing the compiled PK getters, which might be seen as breaking
469
633
  const columns = allParam.length > 1 ? prop.fieldNames : prop.ownColumns;
470
- const newParam = [];
634
+ const param = [];
471
635
  columns.forEach((field, idx) => {
472
636
  if (usedDups.includes(field)) {
473
637
  return;
474
638
  }
475
639
  newFields.push(field);
476
- newParam.push(allParam[idx]);
640
+ param.push(allParam[idx]);
477
641
  });
478
- const param = Utils.flatten(newParam);
479
642
  newFields.forEach((field, idx) => {
480
643
  if (!duplicates.includes(field) || !usedDups.includes(field)) {
481
644
  params.push(param[idx]);
@@ -499,11 +662,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
499
662
  }
500
663
  });
501
664
  return '(' + (keys.join(', ') || 'default') + ')';
502
- }).join(', ');
665
+ })
666
+ .join(', ');
503
667
  }
504
668
  if (meta && this.platform.usesReturningStatement()) {
505
- const returningProps = meta.props
506
- .filter(prop => prop.persist !== false && prop.defaultRaw || prop.autoincrement || prop.generated)
669
+ const returningProps = this.getTableProps(meta)
670
+ .filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
507
671
  .filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
508
672
  const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
509
673
  /* v8 ignore next */
@@ -515,7 +679,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
515
679
  const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
516
680
  let pk;
517
681
  /* v8 ignore next */
518
- if (pks.length > 1) { // owner has composite pk
682
+ if (pks.length > 1) {
683
+ // owner has composite pk
519
684
  pk = data.map(d => Utils.getPrimaryKeyCond(d, pks));
520
685
  }
521
686
  else {
@@ -531,17 +696,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
531
696
  }
532
697
  async nativeUpdate(entityName, where, data, options = {}) {
533
698
  options.convertCustomTypes ??= true;
534
- const meta = this.metadata.find(entityName);
535
- const pks = this.getPrimaryKeyFields(entityName);
536
- const collections = this.extractManyToMany(entityName, data);
699
+ const meta = this.metadata.get(entityName);
700
+ const pks = this.getPrimaryKeyFields(meta);
701
+ const collections = this.extractManyToMany(meta, data);
537
702
  let res = { affectedRows: 0, insertId: 0, row: {} };
538
703
  if (Utils.isPrimaryKey(where) && pks.length === 1) {
539
704
  /* v8 ignore next */
540
- where = { [meta?.primaryKeys[0] ?? pks[0]]: where };
705
+ where = { [meta.primaryKeys[0] ?? pks[0]]: where };
541
706
  }
542
707
  if (Utils.hasObjectKeys(data)) {
543
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext)
544
- .withSchema(this.getSchemaName(meta, options));
708
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
545
709
  if (options.upsert) {
546
710
  /* v8 ignore next */
547
711
  const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(where) ? Utils.keys(where) : meta.primaryKeys);
@@ -556,14 +720,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
556
720
  if (options.onConflictAction === 'ignore') {
557
721
  qb.ignore();
558
722
  }
723
+ if (options.onConflictWhere) {
724
+ qb.where(options.onConflictWhere);
725
+ }
559
726
  }
560
727
  else {
561
728
  qb.update(data).where(where);
562
729
  // reload generated columns and version fields
563
730
  const returning = [];
564
- meta?.props
565
- .filter(prop => (prop.generated && !prop.primary) || prop.version)
566
- .forEach(prop => returning.push(prop.name));
731
+ meta.props.filter(prop => (prop.generated && !prop.primary) || prop.version).forEach(prop => returning.push(prop.name));
567
732
  qb.returning(returning);
568
733
  }
569
734
  res = await this.rethrow(qb.execute('run', false));
@@ -578,7 +743,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
578
743
  options.convertCustomTypes ??= true;
579
744
  const meta = this.metadata.get(entityName);
580
745
  if (options.upsert) {
581
- const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
746
+ const uniqueFields = options.onConflictFields ??
747
+ (Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
582
748
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
583
749
  const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
584
750
  qb.insert(data)
@@ -591,9 +757,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
591
757
  if (options.onConflictAction === 'ignore') {
592
758
  qb.ignore();
593
759
  }
760
+ if (options.onConflictWhere) {
761
+ qb.where(options.onConflictWhere);
762
+ }
594
763
  return this.rethrow(qb.execute('run', false));
595
764
  }
596
- const collections = options.processCollections ? data.map(d => this.extractManyToMany(entityName, d)) : [];
765
+ const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
597
766
  const keys = new Set();
598
767
  const fields = new Set();
599
768
  const returning = new Set();
@@ -606,10 +775,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
606
775
  }
607
776
  }
608
777
  // reload generated columns and version fields
609
- meta?.props
610
- .filter(prop => prop.generated || prop.version || prop.primary)
611
- .forEach(prop => returning.add(prop.name));
612
- const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames)).map(pk => `${this.platform.quoteIdentifier(pk)} = ?`).join(' and ');
778
+ meta.props.filter(prop => prop.generated || prop.version || prop.primary).forEach(prop => returning.add(prop.name));
779
+ const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
780
+ .map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
781
+ .join(' and ');
613
782
  const params = [];
614
783
  let sql = `update ${this.getTableName(meta, options)} set `;
615
784
  const addParams = (prop, value) => {
@@ -628,6 +797,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
628
797
  };
629
798
  for (const key of keys) {
630
799
  const prop = meta.properties[key] ?? meta.root.properties[key];
800
+ if (prop.polymorphic && prop.fieldNames.length > 1) {
801
+ for (let idx = 0; idx < data.length; idx++) {
802
+ const rowValue = data[idx][key];
803
+ if (rowValue instanceof PolymorphicRef) {
804
+ data[idx][key] = rowValue.toTuple();
805
+ }
806
+ }
807
+ }
631
808
  prop.fieldNames.forEach((fieldName, fieldNameIdx) => {
632
809
  if (fields.has(fieldName) || (prop.ownColumns && !prop.ownColumns.includes(fieldName))) {
633
810
  return;
@@ -699,12 +876,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
699
876
  return res;
700
877
  }
701
878
  async nativeDelete(entityName, where, options = {}) {
702
- const meta = this.metadata.find(entityName);
703
- const pks = this.getPrimaryKeyFields(entityName);
879
+ const meta = this.metadata.get(entityName);
880
+ const pks = this.getPrimaryKeyFields(meta);
704
881
  if (Utils.isPrimaryKey(where) && pks.length === 1) {
705
882
  where = { [pks[0]]: where };
706
883
  }
707
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext).delete(where).withSchema(this.getSchemaName(meta, options));
884
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
885
+ .delete(where)
886
+ .withSchema(this.getSchemaName(meta, options));
708
887
  return this.rethrow(qb.execute('run', false));
709
888
  }
710
889
  /**
@@ -753,45 +932,42 @@ export class AbstractSqlDriver extends DatabaseDriver {
753
932
  }
754
933
  if (coll.property.kind === ReferenceKind.ONE_TO_MANY) {
755
934
  const cols = coll.property.referencedColumnNames;
756
- const qb = this.createQueryBuilder(coll.property.type, options?.ctx, 'write')
757
- .withSchema(this.getSchemaName(meta, options));
935
+ const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(this.getSchemaName(meta, options));
758
936
  if (coll.getSnapshot() === undefined) {
759
937
  if (coll.property.orphanRemoval) {
760
- const query = qb.delete({ [coll.property.mappedBy]: pks })
761
- .andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
938
+ const query = qb.delete({ [coll.property.mappedBy]: pks }).andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
762
939
  await this.rethrow(query.execute());
763
940
  continue;
764
941
  }
765
- const query = qb.update({ [coll.property.mappedBy]: null })
942
+ const query = qb
943
+ .update({ [coll.property.mappedBy]: null })
766
944
  .where({ [coll.property.mappedBy]: pks })
767
945
  .andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
768
946
  await this.rethrow(query.execute());
769
947
  continue;
770
948
  }
771
949
  /* v8 ignore next */
772
- const query = qb.update({ [coll.property.mappedBy]: pks })
773
- .where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
950
+ const query = qb.update({ [coll.property.mappedBy]: pks }).where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
774
951
  await this.rethrow(query.execute());
775
952
  continue;
776
953
  }
777
- /* v8 ignore next */
778
954
  const pivotMeta = this.metadata.find(coll.property.pivotEntity);
779
955
  let schema = pivotMeta.schema;
780
956
  if (schema === '*') {
781
957
  if (coll.property.owner) {
782
- schema = wrapped.getSchema() === '*' ? options?.schema ?? this.config.get('schema') : wrapped.getSchema();
958
+ schema = wrapped.getSchema() === '*' ? (options?.schema ?? this.config.get('schema')) : wrapped.getSchema();
783
959
  }
784
960
  else {
785
961
  const targetMeta = coll.property.targetMeta;
786
962
  const targetSchema = (coll[0] ?? snap?.[0]) && helper(coll[0] ?? snap?.[0]).getSchema();
787
- schema = targetMeta.schema === '*' ? options?.schema ?? targetSchema ?? this.config.get('schema') : targetMeta.schema;
963
+ schema = targetMeta.schema === '*' ? (options?.schema ?? targetSchema ?? this.config.get('schema')) : targetMeta.schema;
788
964
  }
789
965
  }
790
966
  else if (schema == null) {
791
967
  schema = this.config.get('schema');
792
968
  }
793
969
  const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
794
- const persister = groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext);
970
+ const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext));
795
971
  persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks, coll.isInitialized());
796
972
  }
797
973
  for (const persister of Utils.values(groups)) {
@@ -799,13 +975,17 @@ export class AbstractSqlDriver extends DatabaseDriver {
799
975
  }
800
976
  }
801
977
  async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
978
+ /* v8 ignore next */
802
979
  if (owners.length === 0) {
803
980
  return {};
804
981
  }
805
- const pivotMeta = this.metadata.find(prop.pivotEntity);
982
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
983
+ if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
984
+ return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
985
+ }
806
986
  const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
807
987
  const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
808
- const ownerMeta = this.metadata.find(pivotProp2.type);
988
+ const ownerMeta = pivotProp2.targetMeta;
809
989
  const cond = {
810
990
  [pivotProp2.name]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
811
991
  };
@@ -817,34 +997,143 @@ export class AbstractSqlDriver extends DatabaseDriver {
817
997
  const populate = this.autoJoinOneToOneOwner(prop.targetMeta, options?.populate ?? [], options?.fields);
818
998
  const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${pivotProp1.name}.${f}`) : [];
819
999
  const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${pivotProp1.name}.${f}`) : [];
820
- const fields = pivotJoin
821
- ? [pivotProp1.name, pivotProp2.name]
822
- : [pivotProp1.name, pivotProp2.name, ...childFields];
823
- const res = await this.find(pivotMeta.className, where, {
1000
+ const fields = pivotJoin ? [pivotProp1.name, pivotProp2.name] : [pivotProp1.name, pivotProp2.name, ...childFields];
1001
+ const res = await this.find(pivotMeta.class, where, {
824
1002
  ctx,
825
1003
  ...options,
826
1004
  fields,
827
1005
  exclude: childExclude,
828
1006
  orderBy: this.getPivotOrderBy(prop, pivotProp1, orderBy, options?.orderBy),
1007
+ populate: [
1008
+ {
1009
+ field: populateField,
1010
+ strategy: LoadStrategy.JOINED,
1011
+ joinType: JoinType.innerJoin,
1012
+ children: populate,
1013
+ dataOnly: pivotProp1.mapToPk && !pivotJoin,
1014
+ },
1015
+ ],
1016
+ populateWhere: undefined,
1017
+ // @ts-ignore
1018
+ _populateWhere: 'infer',
1019
+ populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
1020
+ });
1021
+ return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
1022
+ }
1023
+ /**
1024
+ * Load from a polymorphic M:N pivot table.
1025
+ */
1026
+ async loadFromPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
1027
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
1028
+ // Find the M:1 relation on the pivot pointing to the target entity.
1029
+ // We exclude virtual polymorphic owner relations (persist: false) and non-M:1 relations.
1030
+ const inverseProp = pivotMeta.relations.find(r => r.kind === ReferenceKind.MANY_TO_ONE && r.persist !== false && r.targetMeta === prop.targetMeta);
1031
+ if (inverseProp) {
1032
+ return this.loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp);
1033
+ }
1034
+ return this.loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options);
1035
+ }
1036
+ /**
1037
+ * Load from owner side of polymorphic M:N (e.g., Post -> Tags)
1038
+ */
1039
+ async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
1040
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
1041
+ const targetMeta = prop.targetMeta;
1042
+ // Build condition: discriminator = 'post' AND {discriminator} IN (...)
1043
+ const cond = {
1044
+ [prop.discriminatorColumn]: prop.discriminatorValue,
1045
+ [prop.discriminator]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
1046
+ };
1047
+ if (!Utils.isEmpty(where)) {
1048
+ cond[inverseProp.name] = { ...where };
1049
+ }
1050
+ const populateField = pivotJoin ? `${inverseProp.name}:ref` : inverseProp.name;
1051
+ const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
1052
+ const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${inverseProp.name}.${f}`) : [];
1053
+ const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${inverseProp.name}.${f}`) : [];
1054
+ const fields = pivotJoin
1055
+ ? [inverseProp.name, prop.discriminator, prop.discriminatorColumn]
1056
+ : [inverseProp.name, prop.discriminator, prop.discriminatorColumn, ...childFields];
1057
+ const res = await this.find(pivotMeta.class, cond, {
1058
+ ctx,
1059
+ ...options,
1060
+ fields,
1061
+ exclude: childExclude,
1062
+ orderBy: this.getPivotOrderBy(prop, inverseProp, orderBy, options?.orderBy),
1063
+ populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate, dataOnly: inverseProp.mapToPk && !pivotJoin }],
1064
+ populateWhere: undefined,
1065
+ // @ts-ignore
1066
+ _populateWhere: 'infer',
1067
+ populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
1068
+ });
1069
+ return this.buildPivotResultMap(owners, res, prop.discriminator, inverseProp.name);
1070
+ }
1071
+ /**
1072
+ * Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
1073
+ * Uses single query with join via virtual relation on pivot.
1074
+ */
1075
+ async loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options) {
1076
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
1077
+ const targetMeta = prop.targetMeta;
1078
+ // Find the relation to the entity we're starting from (e.g., Tag_inverse -> Tag)
1079
+ // Exclude virtual polymorphic owner relations (persist: false) - we want the actual M:N inverse relation
1080
+ const tagProp = pivotMeta.relations.find(r => r.persist !== false && r.targetMeta !== targetMeta);
1081
+ // Find the virtual relation to the polymorphic owner (e.g., taggable_Post -> Post)
1082
+ const ownerRelationName = `${prop.discriminator}_${targetMeta.tableName}`;
1083
+ const ownerProp = pivotMeta.properties[ownerRelationName];
1084
+ // Build condition: discriminator = 'post' AND Tag_inverse IN (tagIds)
1085
+ const cond = {
1086
+ [prop.discriminatorColumn]: prop.discriminatorValue,
1087
+ [tagProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
1088
+ };
1089
+ if (!Utils.isEmpty(where)) {
1090
+ cond[ownerRelationName] = { ...where };
1091
+ }
1092
+ const populateField = ownerRelationName;
1093
+ const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
1094
+ const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${ownerRelationName}.${f}`) : [];
1095
+ const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${ownerRelationName}.${f}`) : [];
1096
+ const fields = [ownerRelationName, tagProp.name, prop.discriminatorColumn, ...childFields];
1097
+ const res = await this.find(pivotMeta.class, cond, {
1098
+ ctx,
1099
+ ...options,
1100
+ fields,
1101
+ exclude: childExclude,
1102
+ orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
829
1103
  populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate }],
830
1104
  populateWhere: undefined,
831
1105
  // @ts-ignore
832
1106
  _populateWhere: 'infer',
833
- populateFilter: !Utils.isEmpty(options?.populateFilter) ? { [pivotProp2.name]: options?.populateFilter } : undefined,
1107
+ populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
834
1108
  });
1109
+ return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
1110
+ }
1111
+ /**
1112
+ * Build a map from owner PKs to their related entities from pivot table results.
1113
+ */
1114
+ buildPivotResultMap(owners, results, keyProp, valueProp) {
835
1115
  const map = {};
836
1116
  for (const owner of owners) {
837
1117
  const key = Utils.getPrimaryKeyHash(owner);
838
1118
  map[key] = [];
839
1119
  }
840
- for (const item of res) {
841
- const key = Utils.getPrimaryKeyHash(Utils.asArray(item[pivotProp2.name]));
842
- map[key].push(item[pivotProp1.name]);
1120
+ for (const item of results) {
1121
+ const key = Utils.getPrimaryKeyHash(Utils.asArray(item[keyProp]));
1122
+ const entity = item[valueProp];
1123
+ if (map[key]) {
1124
+ map[key].push(entity);
1125
+ }
843
1126
  }
844
1127
  return map;
845
1128
  }
1129
+ wrapPopulateFilter(options, propName) {
1130
+ if (!Utils.isEmpty(options?.populateFilter) || RawQueryFragment.hasObjectFragments(options?.populateFilter)) {
1131
+ return { [propName]: options?.populateFilter };
1132
+ }
1133
+ return undefined;
1134
+ }
846
1135
  getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
847
- if (!Utils.isEmpty(orderBy)) {
1136
+ if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
848
1137
  return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
849
1138
  }
850
1139
  if (prop.kind === ReferenceKind.MANY_TO_MANY && Utils.asArray(parentOrderBy).some(o => o[prop.name])) {
@@ -852,7 +1141,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
852
1141
  .filter(o => o[prop.name])
853
1142
  .map(o => ({ [pivotProp.name]: o[prop.name] }));
854
1143
  }
855
- if (!Utils.isEmpty(prop.orderBy)) {
1144
+ if (!Utils.isEmpty(prop.orderBy) || RawQueryFragment.hasObjectFragments(prop.orderBy)) {
856
1145
  return Utils.asArray(prop.orderBy).map(o => ({ [pivotProp.name]: o }));
857
1146
  }
858
1147
  if (prop.fixedOrder) {
@@ -865,8 +1154,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
865
1154
  }
866
1155
  async *stream(entityName, where, options) {
867
1156
  options = { populate: [], orderBy: [], ...options };
868
- const meta = this.metadata.find(entityName);
869
- if (meta?.virtual) {
1157
+ const meta = this.metadata.get(entityName);
1158
+ if (meta.virtual) {
870
1159
  yield* this.streamFromVirtual(entityName, where, options);
871
1160
  return;
872
1161
  }
@@ -989,21 +1278,46 @@ export class AbstractSqlDriver extends DatabaseDriver {
989
1278
  const populate = options.populate ?? [];
990
1279
  const joinedProps = this.joinedProps(meta, populate, options);
991
1280
  const populateWhereAll = options?._populateWhere === 'all' || Utils.isEmpty(options?._populateWhere);
1281
+ // Ensure TPT joins are applied early so that _tptAlias is available for join resolution
1282
+ // This is needed when populating relations that are inherited from TPT parent entities
1283
+ if (!options.parentJoinPath) {
1284
+ qb.ensureTPTJoins();
1285
+ }
992
1286
  // root entity is already handled, skip that
993
1287
  if (options.parentJoinPath) {
994
1288
  // alias all fields in the primary table
995
1289
  meta.props
996
1290
  .filter(prop => this.shouldHaveColumn(meta, prop, populate, options.explicitFields, options.exclude))
997
- .forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, options.parentTableAlias, options.explicitFields)));
1291
+ .forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, options.parentTableAlias, meta, options.schema, options.explicitFields)));
998
1292
  }
999
1293
  for (const hint of joinedProps) {
1000
1294
  const [propName, ref] = hint.field.split(':', 2);
1001
1295
  const prop = meta.properties[propName];
1296
+ // Polymorphic to-one: create a LEFT JOIN per target type
1297
+ // Skip :ref hints — polymorphic to-one already has FK + discriminator in the row
1298
+ if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
1299
+ const basePath = options.parentJoinPath ? `${options.parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
1300
+ const pathPrefix = !options.parentJoinPath && populateWhereAll && !basePath.startsWith('[populate]') ? '[populate]' : '';
1301
+ for (const targetMeta of prop.polymorphTargets) {
1302
+ const tableAlias = qb.getNextAlias(targetMeta.className);
1303
+ const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
1304
+ const schema = targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : targetMeta.schema;
1305
+ qb.addPolymorphicJoin(prop, targetMeta, options.parentTableAlias, tableAlias, JoinType.leftJoin, targetPath, schema);
1306
+ // Select fields from each target table
1307
+ fields.push(...this.getFieldsForJoinedLoad(qb, targetMeta, {
1308
+ ...options,
1309
+ populate: hint.children,
1310
+ parentTableAlias: tableAlias,
1311
+ parentJoinPath: targetPath,
1312
+ }));
1313
+ }
1314
+ continue;
1315
+ }
1002
1316
  // ignore ref joins of known FKs unless it's a filter hint
1003
1317
  if (ref && !hint.filter && (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))) {
1004
1318
  continue;
1005
1319
  }
1006
- const meta2 = this.metadata.find(prop.type);
1320
+ const meta2 = prop.targetMeta;
1007
1321
  const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
1008
1322
  const tableAlias = qb.getNextAlias(prop.name);
1009
1323
  const field = `${options.parentTableAlias}.${prop.name}`;
@@ -1019,7 +1333,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
1019
1333
  : (hint.filter && !prop.nullable) || mandatoryToOneProperty
1020
1334
  ? JoinType.innerJoin
1021
1335
  : JoinType.leftJoin;
1022
- qb.join(field, tableAlias, {}, joinType, path);
1336
+ const schema = prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
1337
+ qb.join(field, tableAlias, {}, joinType, path, schema);
1338
+ // For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
1339
+ if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
1340
+ // Use the registry metadata to ensure allTPTDescendants is available
1341
+ const tptMeta = this.metadata.get(meta2.class);
1342
+ this.addTPTPolymorphicJoinsForRelation(qb, tptMeta, tableAlias, fields);
1343
+ }
1023
1344
  if (pivotRefJoin) {
1024
1345
  fields.push(...prop.joinColumns.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)), ...prop.inverseJoinColumns.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
1025
1346
  }
@@ -1040,7 +1361,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1040
1361
  }
1041
1362
  });
1042
1363
  const childExclude = options.exclude ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
1043
- if (!ref && !prop.mapToPk) {
1364
+ if (!ref && (!prop.mapToPk || hint.dataOnly)) {
1044
1365
  fields.push(...this.getFieldsForJoinedLoad(qb, meta2, {
1045
1366
  ...options,
1046
1367
  explicitFields: childExplicitFields.length === 0 ? undefined : childExplicitFields,
@@ -1050,23 +1371,127 @@ export class AbstractSqlDriver extends DatabaseDriver {
1050
1371
  parentJoinPath: path,
1051
1372
  }));
1052
1373
  }
1053
- else if (hint.filter || prop.mapToPk || (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
1374
+ else if (hint.filter || (prop.mapToPk && !hint.dataOnly) || (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
1054
1375
  fields.push(...prop.referencedColumnNames.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
1055
1376
  }
1056
1377
  }
1057
1378
  return fields;
1058
1379
  }
1059
1380
  /**
1381
+ * Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
1060
1382
  * @internal
1061
1383
  */
1062
- mapPropToFieldNames(qb, prop, tableAlias, explicitFields) {
1384
+ addTPTPolymorphicJoinsForRelation(qb, meta, baseAlias, fields) {
1385
+ // allTPTDescendants is pre-computed during discovery, sorted by depth (deepest first)
1386
+ const descendants = meta.allTPTDescendants;
1387
+ const childAliases = {};
1388
+ // LEFT JOIN each descendant table
1389
+ for (const childMeta of descendants) {
1390
+ const childAlias = qb.getNextAlias(childMeta.className);
1391
+ qb.createAlias(childMeta.class, childAlias);
1392
+ childAliases[childMeta.className] = childAlias;
1393
+ qb.addPropertyJoin(childMeta.tptInverseProp, baseAlias, childAlias, JoinType.leftJoin, `[tpt]${meta.className}`);
1394
+ // Add fields from this child (only ownProps, skip PKs)
1395
+ for (const prop of childMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
1396
+ for (const fieldName of prop.fieldNames) {
1397
+ const field = `${childAlias}.${fieldName}`;
1398
+ const fieldAlias = `${childAlias}__${fieldName}`;
1399
+ fields.push(raw(`${this.platform.quoteIdentifier(field)} as ${this.platform.quoteIdentifier(fieldAlias)}`));
1400
+ }
1401
+ }
1402
+ }
1403
+ // Add computed discriminator (descendants already sorted by depth)
1404
+ if (meta.root.tptDiscriminatorColumn) {
1405
+ fields.push(this.buildTPTDiscriminatorExpression(meta, descendants, childAliases, baseAlias));
1406
+ }
1407
+ }
1408
+ /**
1409
+ * Find the alias for a TPT child table in the query builder.
1410
+ * @internal
1411
+ */
1412
+ findTPTChildAlias(qb, childMeta) {
1413
+ const joins = qb._joins;
1414
+ for (const key of Object.keys(joins)) {
1415
+ if (joins[key].table === childMeta.tableName && key.includes('[tpt]')) {
1416
+ return joins[key].alias;
1417
+ }
1418
+ }
1419
+ return undefined;
1420
+ }
1421
+ /**
1422
+ * Builds a CASE WHEN expression for TPT discriminator.
1423
+ * Determines concrete entity type based on which child table has a non-null PK.
1424
+ * @internal
1425
+ */
1426
+ buildTPTDiscriminatorExpression(meta, descendants, aliasMap, baseAlias) {
1427
+ const cases = descendants.map(child => {
1428
+ const childAlias = aliasMap[child.className];
1429
+ const pkFieldName = child.properties[child.primaryKeys[0]].fieldNames[0];
1430
+ return `when ${this.platform.quoteIdentifier(`${childAlias}.${pkFieldName}`)} is not null then '${child.discriminatorValue}'`;
1431
+ });
1432
+ const defaultValue = meta.abstract ? 'null' : `'${meta.discriminatorValue}'`;
1433
+ const caseExpr = `case ${cases.join(' ')} else ${defaultValue} end`;
1434
+ const aliased = this.platform.quoteIdentifier(`${baseAlias}__${meta.root.tptDiscriminatorColumn}`);
1435
+ return raw(`${caseExpr} as ${aliased}`);
1436
+ }
1437
+ /**
1438
+ * Maps TPT child-specific fields during hydration.
1439
+ * When a relation points to a TPT base class, the actual entity might be a child class.
1440
+ * This method reads the discriminator to determine the concrete type and maps child-specific fields.
1441
+ * @internal
1442
+ */
1443
+ mapTPTChildFields(relationPojo, meta, relationAlias, qb, root) {
1444
+ // Check if this is a TPT base with polymorphic children
1445
+ if (meta.inheritanceType !== 'tpt' || !meta.root.tptDiscriminatorColumn) {
1446
+ return;
1447
+ }
1448
+ // Read the discriminator value
1449
+ const discriminatorAlias = `${relationAlias}__${meta.root.tptDiscriminatorColumn}`;
1450
+ const discriminatorValue = root[discriminatorAlias];
1451
+ if (!discriminatorValue) {
1452
+ return;
1453
+ }
1454
+ // Set the discriminator in the pojo for EntityFactory
1455
+ relationPojo[meta.root.tptDiscriminatorColumn] = discriminatorValue;
1456
+ // Find the concrete metadata from discriminator map
1457
+ const concreteClass = meta.root.discriminatorMap?.[discriminatorValue];
1458
+ /* v8 ignore next 3 - defensive check for invalid discriminator values */
1459
+ if (!concreteClass) {
1460
+ return;
1461
+ }
1462
+ const concreteMeta = this.metadata.get(concreteClass);
1463
+ if (concreteMeta === meta) {
1464
+ // Already the concrete type, no child fields to map
1465
+ delete root[discriminatorAlias];
1466
+ return;
1467
+ }
1468
+ // Traverse up from concrete type and map fields from each level's table
1469
+ const tz = this.platform.getTimezone();
1470
+ let currentMeta = concreteMeta;
1471
+ while (currentMeta && currentMeta !== meta) {
1472
+ const childAlias = this.findTPTChildAlias(qb, currentMeta);
1473
+ if (childAlias) {
1474
+ // Map fields using same filtering as joined loading, plus skip PKs
1475
+ for (const prop of currentMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
1476
+ this.mapJoinedProp(relationPojo, prop, childAlias, root, tz, currentMeta, { deleteFromRoot: true });
1477
+ }
1478
+ }
1479
+ currentMeta = currentMeta.tptParent;
1480
+ }
1481
+ // Clean up the discriminator alias
1482
+ delete root[discriminatorAlias];
1483
+ }
1484
+ /**
1485
+ * @internal
1486
+ */
1487
+ mapPropToFieldNames(qb, prop, tableAlias, meta, schema, explicitFields) {
1063
1488
  if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
1064
1489
  return Object.entries(prop.embeddedProps).flatMap(([name, childProp]) => {
1065
1490
  const childFields = explicitFields ? Utils.extractChildElements(explicitFields, prop.name) : [];
1066
1491
  if (!this.shouldHaveColumn(prop.targetMeta, { ...childProp, name }, [], childFields.length > 0 ? childFields : undefined)) {
1067
1492
  return [];
1068
1493
  }
1069
- return this.mapPropToFieldNames(qb, childProp, tableAlias, childFields);
1494
+ return this.mapPropToFieldNames(qb, childProp, tableAlias, meta, schema, childFields);
1070
1495
  });
1071
1496
  }
1072
1497
  const aliased = this.platform.quoteIdentifier(`${tableAlias}__${prop.fieldNames[0]}`);
@@ -1085,11 +1510,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
1085
1510
  return [raw(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
1086
1511
  }
1087
1512
  if (prop.formula) {
1088
- const alias = this.platform.quoteIdentifier(tableAlias);
1089
- return [raw(`${prop.formula(alias)} as ${aliased}`)];
1513
+ const quotedAlias = this.platform.quoteIdentifier(tableAlias).toString();
1514
+ const table = this.createFormulaTable(quotedAlias, meta, schema);
1515
+ const columns = meta.createColumnMappingObject(tableAlias);
1516
+ return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
1090
1517
  }
1091
1518
  return prop.fieldNames.map(fieldName => {
1092
- return `${tableAlias}.${fieldName} as ${tableAlias}__${fieldName}`;
1519
+ return raw('?? as ??', [`${tableAlias}.${fieldName}`, `${tableAlias}__${fieldName}`]);
1093
1520
  });
1094
1521
  }
1095
1522
  /** @internal */
@@ -1114,26 +1541,20 @@ export class AbstractSqlDriver extends DatabaseDriver {
1114
1541
  }
1115
1542
  return 'write';
1116
1543
  }
1117
- extractManyToMany(entityName, data) {
1118
- if (!this.metadata.has(entityName)) {
1119
- return {};
1120
- }
1544
+ extractManyToMany(meta, data) {
1121
1545
  const ret = {};
1122
- this.metadata.find(entityName).relations.forEach(prop => {
1546
+ for (const prop of meta.relations) {
1123
1547
  if (prop.kind === ReferenceKind.MANY_TO_MANY && data[prop.name]) {
1124
1548
  ret[prop.name] = data[prop.name].map((item) => Utils.asArray(item));
1125
1549
  delete data[prop.name];
1126
1550
  }
1127
- });
1551
+ }
1128
1552
  return ret;
1129
1553
  }
1130
1554
  async processManyToMany(meta, pks, collections, clear, options) {
1131
- if (!meta) {
1132
- return;
1133
- }
1134
1555
  for (const prop of meta.relations) {
1135
1556
  if (collections[prop.name]) {
1136
- const pivotMeta = this.metadata.find(prop.pivotEntity);
1557
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
1137
1558
  const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
1138
1559
  persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
1139
1560
  await this.rethrow(persister.execute());
@@ -1142,9 +1563,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
1142
1563
  }
1143
1564
  async lockPessimistic(entity, options) {
1144
1565
  const meta = helper(entity).__meta;
1145
- const qb = this.createQueryBuilder(entity.constructor.name, options.ctx, undefined, undefined, options.logging).withSchema(options.schema ?? meta.schema);
1566
+ const qb = this.createQueryBuilder(meta.class, options.ctx, undefined, undefined, options.logging).withSchema(options.schema ?? meta.schema);
1146
1567
  const cond = Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
1147
- qb.select(raw('1')).where(cond).setLockMode(options.lockMode, options.lockTableAliases);
1568
+ qb.select(raw('1'))
1569
+ .where(cond)
1570
+ .setLockMode(options.lockMode, options.lockTableAliases);
1148
1571
  await this.rethrow(qb.execute());
1149
1572
  }
1150
1573
  buildPopulateWhere(meta, joinedProps, options) {
@@ -1152,21 +1575,24 @@ export class AbstractSqlDriver extends DatabaseDriver {
1152
1575
  for (const hint of joinedProps) {
1153
1576
  const [propName] = hint.field.split(':', 2);
1154
1577
  const prop = meta.properties[propName];
1155
- if (!Utils.isEmpty(prop.where)) {
1578
+ if (!Utils.isEmpty(prop.where) || RawQueryFragment.hasObjectFragments(prop.where)) {
1156
1579
  where[prop.name] = Utils.copy(prop.where);
1157
1580
  }
1158
1581
  if (hint.children) {
1159
- const inner = this.buildPopulateWhere(prop.targetMeta, hint.children, {});
1160
- if (!Utils.isEmpty(inner)) {
1161
- where[prop.name] ??= {};
1162
- Object.assign(where[prop.name], inner);
1582
+ const targetMeta = prop.targetMeta;
1583
+ if (targetMeta) {
1584
+ const inner = this.buildPopulateWhere(targetMeta, hint.children, {});
1585
+ if (!Utils.isEmpty(inner) || RawQueryFragment.hasObjectFragments(inner)) {
1586
+ where[prop.name] ??= {};
1587
+ Object.assign(where[prop.name], inner);
1588
+ }
1163
1589
  }
1164
1590
  }
1165
1591
  }
1166
- if (Utils.isEmpty(options.populateWhere)) {
1592
+ if (Utils.isEmpty(options.populateWhere) && !RawQueryFragment.hasObjectFragments(options.populateWhere)) {
1167
1593
  return where;
1168
1594
  }
1169
- if (Utils.isEmpty(where)) {
1595
+ if (Utils.isEmpty(where) && !RawQueryFragment.hasObjectFragments(where)) {
1170
1596
  return options.populateWhere;
1171
1597
  }
1172
1598
  /* v8 ignore next */
@@ -1178,31 +1604,32 @@ export class AbstractSqlDriver extends DatabaseDriver {
1178
1604
  // as `options.populateWhere` will be always recomputed to respect filters
1179
1605
  const populateWhereAll = options._populateWhere !== 'infer' && !Utils.isEmpty(options._populateWhere);
1180
1606
  const path = (populateWhereAll ? '[populate]' : '') + meta.className;
1607
+ const optionsOrderBy = Utils.asArray(options.orderBy);
1181
1608
  const populateOrderBy = this.buildPopulateOrderBy(qb, meta, Utils.asArray(options.populateOrderBy ?? options.orderBy), path, !!options.populateOrderBy);
1182
1609
  const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta, joinedProps, options, path);
1183
- return [...Utils.asArray(options.orderBy), ...populateOrderBy, ...joinedPropsOrderBy];
1610
+ return [...optionsOrderBy, ...populateOrderBy, ...joinedPropsOrderBy];
1184
1611
  }
1185
1612
  buildPopulateOrderBy(qb, meta, populateOrderBy, parentPath, explicit, parentAlias = qb.alias) {
1186
1613
  const orderBy = [];
1187
1614
  for (let i = 0; i < populateOrderBy.length; i++) {
1188
1615
  const orderHint = populateOrderBy[i];
1189
- for (const propName of Utils.keys(orderHint)) {
1190
- const raw = RawQueryFragment.getKnownFragment(propName, explicit);
1191
- if (raw) {
1192
- const sql = raw.sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), parentAlias);
1193
- const raw2 = new RawQueryFragment(sql, raw.params);
1194
- orderBy.push({ [raw2]: orderHint[propName] });
1616
+ for (const field of Utils.getObjectQueryKeys(orderHint)) {
1617
+ const childOrder = orderHint[field];
1618
+ if (RawQueryFragment.isKnownFragmentSymbol(field)) {
1619
+ const { sql, params } = RawQueryFragment.getKnownFragment(field);
1620
+ const key = raw(sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), parentAlias), params);
1621
+ orderBy.push({ [key]: childOrder });
1195
1622
  continue;
1196
1623
  }
1197
- const prop = meta.properties[propName];
1624
+ const prop = meta.properties[field];
1198
1625
  if (!prop) {
1199
- throw new Error(`Trying to order by not existing property ${meta.className}.${propName}`);
1626
+ throw new Error(`Trying to order by not existing property ${meta.className}.${field}`);
1200
1627
  }
1201
1628
  let path = parentPath;
1202
- const meta2 = this.metadata.find(prop.type);
1203
- const childOrder = orderHint[prop.name];
1204
- if (prop.kind !== ReferenceKind.SCALAR && (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || Utils.isPlainObject(childOrder))) {
1205
- path += `.${propName}`;
1629
+ const meta2 = prop.targetMeta;
1630
+ if (prop.kind !== ReferenceKind.SCALAR &&
1631
+ (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || Utils.isPlainObject(childOrder))) {
1632
+ path += `.${field}`;
1206
1633
  }
1207
1634
  if (prop.kind === ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
1208
1635
  path += '[pivot]';
@@ -1228,9 +1655,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1228
1655
  }
1229
1656
  continue;
1230
1657
  }
1231
- const order = typeof childOrder === 'object' ? childOrder[propName] : childOrder;
1658
+ const order = typeof childOrder === 'object' ? childOrder[field] : childOrder;
1232
1659
  if (order) {
1233
- orderBy.push({ [`${propAlias}.${propName}`]: order });
1660
+ orderBy.push({ [`${propAlias}.${field}`]: order });
1234
1661
  }
1235
1662
  }
1236
1663
  }
@@ -1242,39 +1669,41 @@ export class AbstractSqlDriver extends DatabaseDriver {
1242
1669
  for (const hint of joinedProps) {
1243
1670
  const [propName, ref] = hint.field.split(':', 2);
1244
1671
  const prop = meta.properties[propName];
1245
- const propOrderBy = prop.orderBy;
1246
1672
  let path = `${parentPath}.${propName}`;
1247
1673
  if (prop.kind === ReferenceKind.MANY_TO_MANY && ref) {
1248
1674
  path += '[pivot]';
1249
1675
  }
1250
- const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
1251
- const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
1252
- const meta2 = this.metadata.find(prop.type);
1253
- if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
1254
- const alias = ref ? propAlias : join.ownerAlias;
1255
- orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC });
1256
- }
1257
- if (propOrderBy) {
1258
- for (const item of Utils.asArray(propOrderBy)) {
1259
- for (const field of Utils.keys(item)) {
1260
- const rawField = RawQueryFragment.getKnownFragment(field, false);
1261
- if (rawField) {
1262
- const sql = propAlias ? rawField.sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), propAlias) : rawField.sql;
1263
- const raw2 = raw(sql, rawField.params);
1264
- orderBy.push({ [raw2.toString()]: item[field] });
1265
- continue;
1266
- }
1267
- orderBy.push({ [`${propAlias}.${field}`]: item[field] });
1268
- }
1269
- }
1676
+ if ([ReferenceKind.MANY_TO_MANY, ReferenceKind.ONE_TO_MANY].includes(prop.kind)) {
1677
+ this.buildToManyOrderBy(qb, prop, path, ref, orderBy);
1270
1678
  }
1271
1679
  if (hint.children) {
1272
- const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta2, hint.children, options, path);
1273
- orderBy.push(...buildJoinedPropsOrderBy);
1680
+ orderBy.push(...this.buildJoinedPropsOrderBy(qb, prop.targetMeta, hint.children, options, path));
1274
1681
  }
1275
1682
  }
1276
1683
  return orderBy;
1277
1684
  }
1685
+ buildToManyOrderBy(qb, prop, path, ref, orderBy) {
1686
+ const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
1687
+ const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
1688
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
1689
+ const alias = ref ? propAlias : join.ownerAlias;
1690
+ orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC });
1691
+ }
1692
+ const effectiveOrderBy = QueryHelper.mergeOrderBy(prop.orderBy, prop.targetMeta?.orderBy);
1693
+ for (const item of effectiveOrderBy) {
1694
+ for (const field of Utils.getObjectQueryKeys(item)) {
1695
+ const order = item[field];
1696
+ if (RawQueryFragment.isKnownFragmentSymbol(field)) {
1697
+ const { sql, params } = RawQueryFragment.getKnownFragment(field);
1698
+ const sql2 = propAlias ? sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), propAlias) : sql;
1699
+ const key = raw(sql2, params);
1700
+ orderBy.push({ [key]: order });
1701
+ continue;
1702
+ }
1703
+ orderBy.push({ [`${propAlias}.${field}`]: order });
1704
+ }
1705
+ }
1706
+ }
1278
1707
  normalizeFields(fields, prefix = '') {
1279
1708
  const ret = [];
1280
1709
  for (const field of fields) {
@@ -1313,7 +1742,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1313
1742
  }
1314
1743
  ret.push(prop.name);
1315
1744
  }
1316
- buildFields(meta, populate, joinedProps, qb, alias, options) {
1745
+ buildFields(meta, populate, joinedProps, qb, alias, options, schema) {
1317
1746
  const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => this.isPopulated(meta, prop, p)));
1318
1747
  const hasLazyFormulas = meta.props.some(p => p.lazy && p.formula);
1319
1748
  const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL && p.persist !== false);
@@ -1332,7 +1761,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1332
1761
  const prop = QueryHelper.findProperty(rootPropName, {
1333
1762
  metadata: this.metadata,
1334
1763
  platform: this.platform,
1335
- entityName: meta.className,
1764
+ entityName: meta.class,
1336
1765
  where: {},
1337
1766
  aliasMap: qb.getAliasMap(),
1338
1767
  });
@@ -1341,7 +1770,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1341
1770
  if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
1342
1771
  ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
1343
1772
  }
1344
- if (meta.root.discriminatorColumn && !options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
1773
+ if (meta.root.inheritanceType === 'sti' && !options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
1345
1774
  ret.push(meta.root.discriminatorColumn);
1346
1775
  }
1347
1776
  }
@@ -1358,14 +1787,18 @@ export class AbstractSqlDriver extends DatabaseDriver {
1358
1787
  ret.push('*');
1359
1788
  }
1360
1789
  if (ret.length > 0 && !hasExplicitFields && addFormulas) {
1790
+ // Create formula column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
1791
+ const quotedAlias = this.platform.quoteIdentifier(alias);
1792
+ const columns = meta.createColumnMappingObject(alias);
1793
+ const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
1361
1794
  for (const prop of meta.props) {
1362
1795
  if (lazyProps.includes(prop)) {
1363
1796
  continue;
1364
1797
  }
1365
1798
  if (prop.formula) {
1366
- const a = this.platform.quoteIdentifier(alias);
1367
1799
  const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
1368
- ret.push(raw(`${prop.formula(a)} as ${aliased}`));
1800
+ const table = this.createFormulaTable(quotedAlias.toString(), meta, effectiveSchema);
1801
+ ret.push(raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`));
1369
1802
  }
1370
1803
  if (!prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)) {
1371
1804
  ret.push(prop.name);