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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/AbstractSqlConnection.d.ts +2 -4
  2. package/AbstractSqlConnection.js +3 -7
  3. package/AbstractSqlDriver.d.ts +82 -23
  4. package/AbstractSqlDriver.js +584 -184
  5. package/AbstractSqlPlatform.d.ts +3 -4
  6. package/AbstractSqlPlatform.js +0 -4
  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} +3 -2
  11. package/dialects/mysql/{MySqlPlatform.js → BaseMySqlPlatform.js} +5 -1
  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 +50 -35
  44. package/query/QueryBuilder.d.ts +548 -79
  45. package/query/QueryBuilder.js +537 -159
  46. package/query/QueryBuilderHelper.d.ts +22 -14
  47. package/query/QueryBuilderHelper.js +158 -69
  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 +126 -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 +74 -36
  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,21 @@ 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
+ }
20
35
  createEntityManager(useContext) {
21
36
  const EntityManagerClass = this.config.get('entityManager', SqlEntityManager);
22
37
  return new EntityManagerClass(this.config, this, this.metadata, useContext);
@@ -25,8 +40,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
25
40
  const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
26
41
  const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
27
42
  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);
43
+ const schema = this.getSchemaName(meta, options);
44
+ const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em).withSchema(schema);
45
+ const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
30
46
  const orderBy = this.buildOrderBy(qb, meta, populate, options);
31
47
  const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
32
48
  Utils.asArray(options.flags).forEach(flag => qb.setFlag(flag));
@@ -44,8 +60,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
44
60
  .having(options.having)
45
61
  .indexHint(options.indexHint)
46
62
  .comment(options.comments)
47
- .hintComment(options.hintComments)
48
- .withSchema(this.getSchemaName(meta, options));
63
+ .hintComment(options.hintComments);
49
64
  if (isCursorPagination) {
50
65
  const { orderBy: newOrderBy, where } = this.processCursorOptions(meta, options, orderBy);
51
66
  qb.andWhere(where).orderBy(newOrderBy);
@@ -66,8 +81,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
66
81
  }
67
82
  async find(entityName, where, options = {}) {
68
83
  options = { populate: [], orderBy: [], ...options };
69
- const meta = this.metadata.find(entityName);
70
- if (meta?.virtual) {
84
+ const meta = this.metadata.get(entityName);
85
+ if (meta.virtual) {
71
86
  return this.findVirtual(entityName, where, options);
72
87
  }
73
88
  const qb = await this.createQueryBuilderFromOptions(meta, where, options);
@@ -170,11 +185,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
170
185
  const isCursorPagination = [options.first, options.last, options.before, options.after].some(v => v != null);
171
186
  const native = qb.getNativeQuery(false);
172
187
  if (type === QueryType.COUNT) {
173
- native
174
- .clear('select')
175
- .clear('limit')
176
- .clear('offset')
177
- .count();
188
+ native.clear('select').clear('limit').clear('offset').count();
178
189
  }
179
190
  native.from(raw(`(${expression}) as ${this.platform.quoteIdentifier(qb.alias)}`));
180
191
  const query = native.compile();
@@ -200,6 +211,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
200
211
  }
201
212
  }
202
213
  mapResult(result, meta, populate = [], qb, map = {}) {
214
+ // For TPT inheritance, map aliased parent table columns back to their field names
215
+ if (qb && meta.inheritanceType === 'tpt' && meta.tptParent) {
216
+ this.mapTPTColumns(result, meta, qb);
217
+ }
218
+ // For TPT polymorphic queries (querying a base class), map child table fields
219
+ if (qb && meta.inheritanceType === 'tpt' && meta.allTPTDescendants?.length) {
220
+ const mainAlias = qb.mainAlias?.aliasName ?? 'e0';
221
+ this.mapTPTChildFields(result, meta, mainAlias, qb, result);
222
+ }
203
223
  const ret = super.mapResult(result, meta);
204
224
  /* v8 ignore next */
205
225
  if (!ret) {
@@ -211,6 +231,33 @@ export class AbstractSqlDriver extends DatabaseDriver {
211
231
  }
212
232
  return ret;
213
233
  }
234
+ /**
235
+ * Maps aliased columns from TPT parent tables back to their original field names.
236
+ * TPT parent columns are selected with aliases like `parent_alias__column_name`,
237
+ * and need to be renamed back to `column_name` for the result mapper to work.
238
+ */
239
+ mapTPTColumns(result, meta, qb) {
240
+ const tptAliases = qb._tptAlias;
241
+ // Walk up the TPT hierarchy
242
+ let parentMeta = meta.tptParent;
243
+ while (parentMeta) {
244
+ const parentAlias = tptAliases[parentMeta.className];
245
+ if (parentAlias) {
246
+ // Rename columns from this parent table
247
+ for (const prop of parentMeta.ownProps) {
248
+ for (const fieldName of prop.fieldNames) {
249
+ const aliasedKey = `${parentAlias}__${fieldName}`;
250
+ if (aliasedKey in result) {
251
+ // Copy the value to the unaliased field name and remove the aliased key
252
+ result[fieldName] = result[aliasedKey];
253
+ delete result[aliasedKey];
254
+ }
255
+ }
256
+ }
257
+ }
258
+ parentMeta = parentMeta.tptParent;
259
+ }
260
+ }
214
261
  mapJoinedProps(result, meta, populate, qb, root, map, parentJoinPath) {
215
262
  const joinedProps = this.joinedProps(meta, populate);
216
263
  joinedProps.forEach(hint => {
@@ -220,8 +267,45 @@ export class AbstractSqlDriver extends DatabaseDriver {
220
267
  if (!prop) {
221
268
  return;
222
269
  }
270
+ // Polymorphic to-one: iterate targets, find the matching one, build entity from its columns.
271
+ // Skip :ref hints — no JOINs were created, so the FK reference is already set by the result mapper.
272
+ if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
273
+ const basePath = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
274
+ const pathPrefix = !parentJoinPath ? '[populate]' : '';
275
+ let matched = false;
276
+ for (const targetMeta of prop.polymorphTargets) {
277
+ const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
278
+ const relationAlias = qb.getAliasForJoinPath(targetPath, { matchPopulateJoins: true });
279
+ const meta2 = targetMeta;
280
+ const targetProps = meta2.props.filter(p => this.platform.shouldHaveColumn(p, hint.children || []));
281
+ const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => root[`${relationAlias}__${name}`] != null));
282
+ if (hasPK && !matched) {
283
+ matched = true;
284
+ let relationPojo = {};
285
+ const tz = this.platform.getTimezone();
286
+ for (const p of targetProps) {
287
+ this.mapJoinedProp(relationPojo, p, relationAlias, root, tz, meta2);
288
+ }
289
+ // Inject the entity class constructor so that the factory creates the correct type
290
+ Object.defineProperty(relationPojo, 'constructor', { value: meta2.class, enumerable: false, configurable: true });
291
+ result[prop.name] = relationPojo;
292
+ const populateChildren = hint.children || [];
293
+ this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, targetPath);
294
+ }
295
+ // Clean up aliased columns for ALL targets (even non-matching ones)
296
+ for (const p of targetProps) {
297
+ for (const name of p.fieldNames) {
298
+ delete root[`${relationAlias}__${name}`];
299
+ }
300
+ }
301
+ }
302
+ if (!matched) {
303
+ result[prop.name] = null;
304
+ }
305
+ return;
306
+ }
223
307
  const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
224
- const meta2 = this.metadata.find(prop.type);
308
+ const meta2 = prop.targetMeta;
225
309
  let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
226
310
  if (!parentJoinPath) {
227
311
  path = '[populate]' + path;
@@ -237,7 +321,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
237
321
  // pivot ref joins via joined strategy need to be handled separately here, as they dont join the target entity
238
322
  if (pivotRefJoin) {
239
323
  let item;
240
- if (prop.inverseJoinColumns.length > 1) { // composite keys
324
+ if (prop.inverseJoinColumns.length > 1) {
325
+ // composite keys
241
326
  item = prop.inverseJoinColumns.map(name => root[`${relationAlias}__${name}`]);
242
327
  }
243
328
  else {
@@ -252,10 +337,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
252
337
  }
253
338
  return;
254
339
  }
255
- const mapToPk = !!(ref || prop.mapToPk);
256
- const targetProps = mapToPk
257
- ? meta2.getPrimaryProps()
258
- : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
340
+ const mapToPk = !hint.dataOnly && !!(ref || prop.mapToPk);
341
+ const targetProps = mapToPk ? meta2.getPrimaryProps() : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
259
342
  // If the primary key value for the relation is null, we know we haven't joined to anything
260
343
  // and therefore we don't return any record (since all values would be null)
261
344
  const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => {
@@ -280,7 +363,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
280
363
  .filter(prop => !ref && prop.persist === false && prop.fieldNames)
281
364
  .forEach(prop => {
282
365
  /* v8 ignore next */
283
- if (prop.fieldNames.length > 1) { // composite keys
366
+ if (prop.fieldNames.length > 1) {
367
+ // composite keys
284
368
  relationPojo[prop.name] = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
285
369
  }
286
370
  else {
@@ -290,41 +374,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
290
374
  });
291
375
  const tz = this.platform.getTimezone();
292
376
  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
- }
377
+ this.mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta2);
327
378
  }
379
+ // Handle TPT polymorphic child fields - map fields from child table aliases
380
+ this.mapTPTChildFields(relationPojo, meta2, relationAlias, qb, root);
328
381
  // properties can be mapped to multiple places, e.g. when sharing a column in multiple FKs,
329
382
  // so we need to delete them after everything is mapped from given level
330
383
  for (const prop of targetProps) {
@@ -348,6 +401,65 @@ export class AbstractSqlDriver extends DatabaseDriver {
348
401
  this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
349
402
  });
350
403
  }
404
+ /**
405
+ * Maps a single property from a joined result row into the relation pojo.
406
+ * Handles polymorphic FKs, composite keys, Date parsing, and embedded objects.
407
+ */
408
+ mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta, options) {
409
+ if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
410
+ return;
411
+ }
412
+ if (prop.polymorphic) {
413
+ const discriminatorAlias = `${relationAlias}__${prop.fieldNames[0]}`;
414
+ const discriminatorValue = root[discriminatorAlias];
415
+ const pkFieldNames = prop.fieldNames.slice(1);
416
+ const pkValues = pkFieldNames.map(name => root[`${relationAlias}__${name}`]);
417
+ const pkValue = pkValues.length === 1 ? pkValues[0] : pkValues;
418
+ if (discriminatorValue != null && pkValue != null) {
419
+ relationPojo[prop.name] = new PolymorphicRef(discriminatorValue, pkValue);
420
+ }
421
+ else {
422
+ relationPojo[prop.name] = null;
423
+ }
424
+ }
425
+ else if (prop.fieldNames.length > 1) {
426
+ // composite keys
427
+ const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
428
+ const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
429
+ relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
430
+ }
431
+ else if (prop.runtimeType === 'Date') {
432
+ const alias = `${relationAlias}__${prop.fieldNames[0]}`;
433
+ const value = root[alias];
434
+ if (tz && tz !== 'local' && typeof value === 'string' && !value.includes('+') && value.lastIndexOf('-') < 11 && !value.endsWith('Z')) {
435
+ relationPojo[prop.name] = this.platform.parseDate(value + tz);
436
+ }
437
+ else if (['string', 'number'].includes(typeof value)) {
438
+ relationPojo[prop.name] = this.platform.parseDate(value);
439
+ }
440
+ else {
441
+ relationPojo[prop.name] = value;
442
+ }
443
+ }
444
+ else {
445
+ const alias = `${relationAlias}__${prop.fieldNames[0]}`;
446
+ relationPojo[prop.name] = root[alias];
447
+ if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
448
+ const item = parseJsonSafe(relationPojo[prop.name]);
449
+ if (Array.isArray(item)) {
450
+ relationPojo[prop.name] = item.map(row => (row == null ? row : this.comparator.mapResult(prop.targetMeta, row)));
451
+ }
452
+ else {
453
+ relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
454
+ }
455
+ }
456
+ }
457
+ if (options?.deleteFromRoot) {
458
+ for (const name of prop.fieldNames) {
459
+ delete root[`${relationAlias}__${name}`];
460
+ }
461
+ }
462
+ }
351
463
  async count(entityName, where, options = {}) {
352
464
  const meta = this.metadata.get(entityName);
353
465
  if (meta.virtual) {
@@ -356,10 +468,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
356
468
  options = { populate: [], ...options };
357
469
  const populate = options.populate;
358
470
  const joinedProps = this.joinedProps(meta, populate, options);
471
+ const schema = this.getSchemaName(meta, options);
359
472
  const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
360
473
  const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
361
474
  if (meta && !Utils.isEmpty(populate)) {
362
- this.buildFields(meta, populate, joinedProps, qb, qb.alias, options);
475
+ this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
363
476
  }
364
477
  qb.__populateWhere = options._populateWhere;
365
478
  qb.indexHint(options.indexHint)
@@ -368,7 +481,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
368
481
  .groupBy(options.groupBy)
369
482
  .having(options.having)
370
483
  .populate(populate, joinedProps.length > 0 ? populateWhere : undefined, joinedProps.length > 0 ? options.populateFilter : undefined)
371
- .withSchema(this.getSchemaName(meta, options))
484
+ .withSchema(schema)
372
485
  .where(where);
373
486
  if (options.em) {
374
487
  await qb.applyJoinedFilters(options.em, options.filters);
@@ -377,19 +490,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
377
490
  }
378
491
  async nativeInsert(entityName, data, options = {}) {
379
492
  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()];
493
+ const meta = this.metadata.get(entityName);
494
+ const collections = this.extractManyToMany(meta, data);
383
495
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
384
496
  const res = await this.rethrow(qb.insert(data).execute('run', false));
385
497
  res.row = res.row || {};
386
498
  let pk;
387
- if (pks.length > 1) { // owner has composite pk
388
- pk = Utils.getPrimaryKeyCond(data, pks);
499
+ if (meta.primaryKeys.length > 1) {
500
+ // owner has composite pk
501
+ pk = Utils.getPrimaryKeyCond(data, meta.primaryKeys);
389
502
  }
390
503
  else {
391
504
  /* v8 ignore next */
392
- res.insertId = data[pks[0]] ?? res.insertId ?? res.row[pks[0]];
505
+ res.insertId = data[meta.primaryKeys[0]] ?? res.insertId ?? res.row[meta.primaryKeys[0]];
393
506
  pk = [res.insertId];
394
507
  }
395
508
  await this.processManyToMany(meta, pk, collections, false, options);
@@ -398,25 +511,26 @@ export class AbstractSqlDriver extends DatabaseDriver {
398
511
  async nativeInsertMany(entityName, data, options = {}, transform) {
399
512
  options.processCollections ??= true;
400
513
  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);
514
+ const entityMeta = this.metadata.get(entityName);
515
+ const meta = entityMeta.inheritanceType === 'tpt' ? entityMeta : entityMeta.root;
516
+ const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
517
+ const pks = this.getPrimaryKeyFields(meta);
404
518
  const set = new Set();
405
519
  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));
520
+ const props = [...set].map(name => meta.properties[name] ?? { name, fieldNames: [name] });
521
+ // For STI with conflicting fieldNames, include all alternative columns
522
+ let fields = Utils.flatten(props.map(prop => prop.stiFieldNames ?? prop.fieldNames));
408
523
  const duplicates = Utils.findDuplicates(fields);
409
524
  const params = [];
410
525
  if (duplicates.length) {
411
526
  fields = Utils.unique(fields);
412
527
  }
413
- /* v8 ignore next */
414
- const tableName = meta ? this.getTableName(meta, options) : this.platform.quoteIdentifier(entityName);
528
+ const tableName = this.getTableName(meta, options);
415
529
  let sql = `insert into ${tableName} `;
416
530
  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)
531
+ if (this.platform.usesOutputStatement()) {
532
+ const returningProps = this.getTableProps(meta)
533
+ .filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
420
534
  .filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
421
535
  const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
422
536
  sql += returningFields.length > 0 ? ` output ${returningFields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}` : '';
@@ -457,25 +571,44 @@ export class AbstractSqlDriver extends DatabaseDriver {
457
571
  params.push(value);
458
572
  };
459
573
  if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
460
- sql += data.map(row => {
574
+ sql += data
575
+ .map(row => {
461
576
  const keys = [];
462
577
  const usedDups = [];
463
578
  props.forEach(prop => {
579
+ // For STI with conflicting fieldNames, use discriminator to determine which field gets value
580
+ if (prop.stiFieldNames && prop.stiFieldNameMap && meta.discriminatorColumn) {
581
+ const activeField = prop.stiFieldNameMap[row[meta.discriminatorColumn]];
582
+ for (const field of prop.stiFieldNames) {
583
+ params.push(field === activeField ? row[prop.name] : null);
584
+ keys.push('?');
585
+ }
586
+ return;
587
+ }
464
588
  if (prop.fieldNames.length > 1) {
465
589
  const newFields = [];
466
- const allParam = [...(Utils.asArray(row[prop.name]) ?? prop.fieldNames.map(() => null))];
590
+ let rawParam;
591
+ const target = row[prop.name];
592
+ if (prop.polymorphic && target instanceof PolymorphicRef) {
593
+ rawParam = target.toTuple();
594
+ }
595
+ else {
596
+ rawParam = Utils.asArray(target) ?? prop.fieldNames.map(() => null);
597
+ }
598
+ // Deep flatten nested arrays when needed (for deeply nested composite keys like Tag -> Comment -> Post -> User)
599
+ const needsFlatten = rawParam.length !== prop.fieldNames.length && rawParam.some(v => Array.isArray(v));
600
+ const allParam = needsFlatten ? Utils.flatten(rawParam, true) : rawParam;
467
601
  // TODO(v7): instead of making this conditional here, the entity snapshot should respect `ownColumns`,
468
602
  // but that means changing the compiled PK getters, which might be seen as breaking
469
603
  const columns = allParam.length > 1 ? prop.fieldNames : prop.ownColumns;
470
- const newParam = [];
604
+ const param = [];
471
605
  columns.forEach((field, idx) => {
472
606
  if (usedDups.includes(field)) {
473
607
  return;
474
608
  }
475
609
  newFields.push(field);
476
- newParam.push(allParam[idx]);
610
+ param.push(allParam[idx]);
477
611
  });
478
- const param = Utils.flatten(newParam);
479
612
  newFields.forEach((field, idx) => {
480
613
  if (!duplicates.includes(field) || !usedDups.includes(field)) {
481
614
  params.push(param[idx]);
@@ -499,11 +632,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
499
632
  }
500
633
  });
501
634
  return '(' + (keys.join(', ') || 'default') + ')';
502
- }).join(', ');
635
+ })
636
+ .join(', ');
503
637
  }
504
638
  if (meta && this.platform.usesReturningStatement()) {
505
- const returningProps = meta.props
506
- .filter(prop => prop.persist !== false && prop.defaultRaw || prop.autoincrement || prop.generated)
639
+ const returningProps = this.getTableProps(meta)
640
+ .filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
507
641
  .filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
508
642
  const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
509
643
  /* v8 ignore next */
@@ -515,7 +649,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
515
649
  const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
516
650
  let pk;
517
651
  /* v8 ignore next */
518
- if (pks.length > 1) { // owner has composite pk
652
+ if (pks.length > 1) {
653
+ // owner has composite pk
519
654
  pk = data.map(d => Utils.getPrimaryKeyCond(d, pks));
520
655
  }
521
656
  else {
@@ -531,17 +666,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
531
666
  }
532
667
  async nativeUpdate(entityName, where, data, options = {}) {
533
668
  options.convertCustomTypes ??= true;
534
- const meta = this.metadata.find(entityName);
535
- const pks = this.getPrimaryKeyFields(entityName);
536
- const collections = this.extractManyToMany(entityName, data);
669
+ const meta = this.metadata.get(entityName);
670
+ const pks = this.getPrimaryKeyFields(meta);
671
+ const collections = this.extractManyToMany(meta, data);
537
672
  let res = { affectedRows: 0, insertId: 0, row: {} };
538
673
  if (Utils.isPrimaryKey(where) && pks.length === 1) {
539
674
  /* v8 ignore next */
540
- where = { [meta?.primaryKeys[0] ?? pks[0]]: where };
675
+ where = { [meta.primaryKeys[0] ?? pks[0]]: where };
541
676
  }
542
677
  if (Utils.hasObjectKeys(data)) {
543
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext)
544
- .withSchema(this.getSchemaName(meta, options));
678
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
545
679
  if (options.upsert) {
546
680
  /* v8 ignore next */
547
681
  const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(where) ? Utils.keys(where) : meta.primaryKeys);
@@ -556,14 +690,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
556
690
  if (options.onConflictAction === 'ignore') {
557
691
  qb.ignore();
558
692
  }
693
+ if (options.onConflictWhere) {
694
+ qb.where(options.onConflictWhere);
695
+ }
559
696
  }
560
697
  else {
561
698
  qb.update(data).where(where);
562
699
  // reload generated columns and version fields
563
700
  const returning = [];
564
- meta?.props
565
- .filter(prop => (prop.generated && !prop.primary) || prop.version)
566
- .forEach(prop => returning.push(prop.name));
701
+ meta.props.filter(prop => (prop.generated && !prop.primary) || prop.version).forEach(prop => returning.push(prop.name));
567
702
  qb.returning(returning);
568
703
  }
569
704
  res = await this.rethrow(qb.execute('run', false));
@@ -578,7 +713,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
578
713
  options.convertCustomTypes ??= true;
579
714
  const meta = this.metadata.get(entityName);
580
715
  if (options.upsert) {
581
- const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
716
+ const uniqueFields = options.onConflictFields ??
717
+ (Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
582
718
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
583
719
  const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
584
720
  qb.insert(data)
@@ -591,9 +727,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
591
727
  if (options.onConflictAction === 'ignore') {
592
728
  qb.ignore();
593
729
  }
730
+ if (options.onConflictWhere) {
731
+ qb.where(options.onConflictWhere);
732
+ }
594
733
  return this.rethrow(qb.execute('run', false));
595
734
  }
596
- const collections = options.processCollections ? data.map(d => this.extractManyToMany(entityName, d)) : [];
735
+ const collections = options.processCollections ? data.map(d => this.extractManyToMany(meta, d)) : [];
597
736
  const keys = new Set();
598
737
  const fields = new Set();
599
738
  const returning = new Set();
@@ -606,10 +745,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
606
745
  }
607
746
  }
608
747
  // 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 ');
748
+ meta.props.filter(prop => prop.generated || prop.version || prop.primary).forEach(prop => returning.add(prop.name));
749
+ const pkCond = Utils.flatten(meta.primaryKeys.map(pk => meta.properties[pk].fieldNames))
750
+ .map(pk => `${this.platform.quoteIdentifier(pk)} = ?`)
751
+ .join(' and ');
613
752
  const params = [];
614
753
  let sql = `update ${this.getTableName(meta, options)} set `;
615
754
  const addParams = (prop, value) => {
@@ -628,6 +767,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
628
767
  };
629
768
  for (const key of keys) {
630
769
  const prop = meta.properties[key] ?? meta.root.properties[key];
770
+ if (prop.polymorphic && prop.fieldNames.length > 1) {
771
+ for (let idx = 0; idx < data.length; idx++) {
772
+ const rowValue = data[idx][key];
773
+ if (rowValue instanceof PolymorphicRef) {
774
+ data[idx][key] = rowValue.toTuple();
775
+ }
776
+ }
777
+ }
631
778
  prop.fieldNames.forEach((fieldName, fieldNameIdx) => {
632
779
  if (fields.has(fieldName) || (prop.ownColumns && !prop.ownColumns.includes(fieldName))) {
633
780
  return;
@@ -699,12 +846,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
699
846
  return res;
700
847
  }
701
848
  async nativeDelete(entityName, where, options = {}) {
702
- const meta = this.metadata.find(entityName);
703
- const pks = this.getPrimaryKeyFields(entityName);
849
+ const meta = this.metadata.get(entityName);
850
+ const pks = this.getPrimaryKeyFields(meta);
704
851
  if (Utils.isPrimaryKey(where) && pks.length === 1) {
705
852
  where = { [pks[0]]: where };
706
853
  }
707
- const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext).delete(where).withSchema(this.getSchemaName(meta, options));
854
+ const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
855
+ .delete(where)
856
+ .withSchema(this.getSchemaName(meta, options));
708
857
  return this.rethrow(qb.execute('run', false));
709
858
  }
710
859
  /**
@@ -753,45 +902,42 @@ export class AbstractSqlDriver extends DatabaseDriver {
753
902
  }
754
903
  if (coll.property.kind === ReferenceKind.ONE_TO_MANY) {
755
904
  const cols = coll.property.referencedColumnNames;
756
- const qb = this.createQueryBuilder(coll.property.type, options?.ctx, 'write')
757
- .withSchema(this.getSchemaName(meta, options));
905
+ const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(this.getSchemaName(meta, options));
758
906
  if (coll.getSnapshot() === undefined) {
759
907
  if (coll.property.orphanRemoval) {
760
- const query = qb.delete({ [coll.property.mappedBy]: pks })
761
- .andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
908
+ const query = qb.delete({ [coll.property.mappedBy]: pks }).andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
762
909
  await this.rethrow(query.execute());
763
910
  continue;
764
911
  }
765
- const query = qb.update({ [coll.property.mappedBy]: null })
912
+ const query = qb
913
+ .update({ [coll.property.mappedBy]: null })
766
914
  .where({ [coll.property.mappedBy]: pks })
767
915
  .andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
768
916
  await this.rethrow(query.execute());
769
917
  continue;
770
918
  }
771
919
  /* v8 ignore next */
772
- const query = qb.update({ [coll.property.mappedBy]: pks })
773
- .where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
920
+ const query = qb.update({ [coll.property.mappedBy]: pks }).where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
774
921
  await this.rethrow(query.execute());
775
922
  continue;
776
923
  }
777
- /* v8 ignore next */
778
924
  const pivotMeta = this.metadata.find(coll.property.pivotEntity);
779
925
  let schema = pivotMeta.schema;
780
926
  if (schema === '*') {
781
927
  if (coll.property.owner) {
782
- schema = wrapped.getSchema() === '*' ? options?.schema ?? this.config.get('schema') : wrapped.getSchema();
928
+ schema = wrapped.getSchema() === '*' ? (options?.schema ?? this.config.get('schema')) : wrapped.getSchema();
783
929
  }
784
930
  else {
785
931
  const targetMeta = coll.property.targetMeta;
786
932
  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;
933
+ schema = targetMeta.schema === '*' ? (options?.schema ?? targetSchema ?? this.config.get('schema')) : targetMeta.schema;
788
934
  }
789
935
  }
790
936
  else if (schema == null) {
791
937
  schema = this.config.get('schema');
792
938
  }
793
939
  const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
794
- const persister = groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext);
940
+ const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext));
795
941
  persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks, coll.isInitialized());
796
942
  }
797
943
  for (const persister of Utils.values(groups)) {
@@ -799,13 +945,17 @@ export class AbstractSqlDriver extends DatabaseDriver {
799
945
  }
800
946
  }
801
947
  async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
948
+ /* v8 ignore next */
802
949
  if (owners.length === 0) {
803
950
  return {};
804
951
  }
805
- const pivotMeta = this.metadata.find(prop.pivotEntity);
952
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
953
+ if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
954
+ return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
955
+ }
806
956
  const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
807
957
  const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
808
- const ownerMeta = this.metadata.find(pivotProp2.type);
958
+ const ownerMeta = pivotProp2.targetMeta;
809
959
  const cond = {
810
960
  [pivotProp2.name]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
811
961
  };
@@ -817,34 +967,143 @@ export class AbstractSqlDriver extends DatabaseDriver {
817
967
  const populate = this.autoJoinOneToOneOwner(prop.targetMeta, options?.populate ?? [], options?.fields);
818
968
  const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${pivotProp1.name}.${f}`) : [];
819
969
  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, {
970
+ const fields = pivotJoin ? [pivotProp1.name, pivotProp2.name] : [pivotProp1.name, pivotProp2.name, ...childFields];
971
+ const res = await this.find(pivotMeta.class, where, {
824
972
  ctx,
825
973
  ...options,
826
974
  fields,
827
975
  exclude: childExclude,
828
976
  orderBy: this.getPivotOrderBy(prop, pivotProp1, orderBy, options?.orderBy),
977
+ populate: [
978
+ {
979
+ field: populateField,
980
+ strategy: LoadStrategy.JOINED,
981
+ joinType: JoinType.innerJoin,
982
+ children: populate,
983
+ dataOnly: pivotProp1.mapToPk && !pivotJoin,
984
+ },
985
+ ],
986
+ populateWhere: undefined,
987
+ // @ts-ignore
988
+ _populateWhere: 'infer',
989
+ populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
990
+ });
991
+ return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
992
+ }
993
+ /**
994
+ * Load from a polymorphic M:N pivot table.
995
+ */
996
+ async loadFromPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
997
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
998
+ // Find the M:1 relation on the pivot pointing to the target entity.
999
+ // We exclude virtual polymorphic owner relations (persist: false) and non-M:1 relations.
1000
+ const inverseProp = pivotMeta.relations.find(r => r.kind === ReferenceKind.MANY_TO_ONE && r.persist !== false && r.targetMeta === prop.targetMeta);
1001
+ if (inverseProp) {
1002
+ return this.loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp);
1003
+ }
1004
+ return this.loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options);
1005
+ }
1006
+ /**
1007
+ * Load from owner side of polymorphic M:N (e.g., Post -> Tags)
1008
+ */
1009
+ async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
1010
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
1011
+ const targetMeta = prop.targetMeta;
1012
+ // Build condition: discriminator = 'post' AND {discriminator} IN (...)
1013
+ const cond = {
1014
+ [prop.discriminatorColumn]: prop.discriminatorValue,
1015
+ [prop.discriminator]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
1016
+ };
1017
+ if (!Utils.isEmpty(where)) {
1018
+ cond[inverseProp.name] = { ...where };
1019
+ }
1020
+ const populateField = pivotJoin ? `${inverseProp.name}:ref` : inverseProp.name;
1021
+ const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
1022
+ const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${inverseProp.name}.${f}`) : [];
1023
+ const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${inverseProp.name}.${f}`) : [];
1024
+ const fields = pivotJoin
1025
+ ? [inverseProp.name, prop.discriminator, prop.discriminatorColumn]
1026
+ : [inverseProp.name, prop.discriminator, prop.discriminatorColumn, ...childFields];
1027
+ const res = await this.find(pivotMeta.class, cond, {
1028
+ ctx,
1029
+ ...options,
1030
+ fields,
1031
+ exclude: childExclude,
1032
+ orderBy: this.getPivotOrderBy(prop, inverseProp, orderBy, options?.orderBy),
1033
+ populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate, dataOnly: inverseProp.mapToPk && !pivotJoin }],
1034
+ populateWhere: undefined,
1035
+ // @ts-ignore
1036
+ _populateWhere: 'infer',
1037
+ populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
1038
+ });
1039
+ return this.buildPivotResultMap(owners, res, prop.discriminator, inverseProp.name);
1040
+ }
1041
+ /**
1042
+ * Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
1043
+ * Uses single query with join via virtual relation on pivot.
1044
+ */
1045
+ async loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options) {
1046
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
1047
+ const targetMeta = prop.targetMeta;
1048
+ // Find the relation to the entity we're starting from (e.g., Tag_inverse -> Tag)
1049
+ // Exclude virtual polymorphic owner relations (persist: false) - we want the actual M:N inverse relation
1050
+ const tagProp = pivotMeta.relations.find(r => r.persist !== false && r.targetMeta !== targetMeta);
1051
+ // Find the virtual relation to the polymorphic owner (e.g., taggable_Post -> Post)
1052
+ const ownerRelationName = `${prop.discriminator}_${targetMeta.tableName}`;
1053
+ const ownerProp = pivotMeta.properties[ownerRelationName];
1054
+ // Build condition: discriminator = 'post' AND Tag_inverse IN (tagIds)
1055
+ const cond = {
1056
+ [prop.discriminatorColumn]: prop.discriminatorValue,
1057
+ [tagProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
1058
+ };
1059
+ if (!Utils.isEmpty(where)) {
1060
+ cond[ownerRelationName] = { ...where };
1061
+ }
1062
+ const populateField = ownerRelationName;
1063
+ const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
1064
+ const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${ownerRelationName}.${f}`) : [];
1065
+ const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${ownerRelationName}.${f}`) : [];
1066
+ const fields = [ownerRelationName, tagProp.name, prop.discriminatorColumn, ...childFields];
1067
+ const res = await this.find(pivotMeta.class, cond, {
1068
+ ctx,
1069
+ ...options,
1070
+ fields,
1071
+ exclude: childExclude,
1072
+ orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
829
1073
  populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate }],
830
1074
  populateWhere: undefined,
831
1075
  // @ts-ignore
832
1076
  _populateWhere: 'infer',
833
- populateFilter: !Utils.isEmpty(options?.populateFilter) ? { [pivotProp2.name]: options?.populateFilter } : undefined,
1077
+ populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
834
1078
  });
1079
+ return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
1080
+ }
1081
+ /**
1082
+ * Build a map from owner PKs to their related entities from pivot table results.
1083
+ */
1084
+ buildPivotResultMap(owners, results, keyProp, valueProp) {
835
1085
  const map = {};
836
1086
  for (const owner of owners) {
837
1087
  const key = Utils.getPrimaryKeyHash(owner);
838
1088
  map[key] = [];
839
1089
  }
840
- for (const item of res) {
841
- const key = Utils.getPrimaryKeyHash(Utils.asArray(item[pivotProp2.name]));
842
- map[key].push(item[pivotProp1.name]);
1090
+ for (const item of results) {
1091
+ const key = Utils.getPrimaryKeyHash(Utils.asArray(item[keyProp]));
1092
+ const entity = item[valueProp];
1093
+ if (map[key]) {
1094
+ map[key].push(entity);
1095
+ }
843
1096
  }
844
1097
  return map;
845
1098
  }
1099
+ wrapPopulateFilter(options, propName) {
1100
+ if (!Utils.isEmpty(options?.populateFilter) || RawQueryFragment.hasObjectFragments(options?.populateFilter)) {
1101
+ return { [propName]: options?.populateFilter };
1102
+ }
1103
+ return undefined;
1104
+ }
846
1105
  getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
847
- if (!Utils.isEmpty(orderBy)) {
1106
+ if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
848
1107
  return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
849
1108
  }
850
1109
  if (prop.kind === ReferenceKind.MANY_TO_MANY && Utils.asArray(parentOrderBy).some(o => o[prop.name])) {
@@ -852,7 +1111,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
852
1111
  .filter(o => o[prop.name])
853
1112
  .map(o => ({ [pivotProp.name]: o[prop.name] }));
854
1113
  }
855
- if (!Utils.isEmpty(prop.orderBy)) {
1114
+ if (!Utils.isEmpty(prop.orderBy) || RawQueryFragment.hasObjectFragments(prop.orderBy)) {
856
1115
  return Utils.asArray(prop.orderBy).map(o => ({ [pivotProp.name]: o }));
857
1116
  }
858
1117
  if (prop.fixedOrder) {
@@ -865,8 +1124,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
865
1124
  }
866
1125
  async *stream(entityName, where, options) {
867
1126
  options = { populate: [], orderBy: [], ...options };
868
- const meta = this.metadata.find(entityName);
869
- if (meta?.virtual) {
1127
+ const meta = this.metadata.get(entityName);
1128
+ if (meta.virtual) {
870
1129
  yield* this.streamFromVirtual(entityName, where, options);
871
1130
  return;
872
1131
  }
@@ -989,21 +1248,46 @@ export class AbstractSqlDriver extends DatabaseDriver {
989
1248
  const populate = options.populate ?? [];
990
1249
  const joinedProps = this.joinedProps(meta, populate, options);
991
1250
  const populateWhereAll = options?._populateWhere === 'all' || Utils.isEmpty(options?._populateWhere);
1251
+ // Ensure TPT joins are applied early so that _tptAlias is available for join resolution
1252
+ // This is needed when populating relations that are inherited from TPT parent entities
1253
+ if (!options.parentJoinPath) {
1254
+ qb.ensureTPTJoins();
1255
+ }
992
1256
  // root entity is already handled, skip that
993
1257
  if (options.parentJoinPath) {
994
1258
  // alias all fields in the primary table
995
1259
  meta.props
996
1260
  .filter(prop => this.shouldHaveColumn(meta, prop, populate, options.explicitFields, options.exclude))
997
- .forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, options.parentTableAlias, options.explicitFields)));
1261
+ .forEach(prop => fields.push(...this.mapPropToFieldNames(qb, prop, options.parentTableAlias, meta, options.schema, options.explicitFields)));
998
1262
  }
999
1263
  for (const hint of joinedProps) {
1000
1264
  const [propName, ref] = hint.field.split(':', 2);
1001
1265
  const prop = meta.properties[propName];
1266
+ // Polymorphic to-one: create a LEFT JOIN per target type
1267
+ // Skip :ref hints — polymorphic to-one already has FK + discriminator in the row
1268
+ if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
1269
+ const basePath = options.parentJoinPath ? `${options.parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
1270
+ const pathPrefix = !options.parentJoinPath && populateWhereAll && !basePath.startsWith('[populate]') ? '[populate]' : '';
1271
+ for (const targetMeta of prop.polymorphTargets) {
1272
+ const tableAlias = qb.getNextAlias(targetMeta.className);
1273
+ const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
1274
+ const schema = targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : targetMeta.schema;
1275
+ qb.addPolymorphicJoin(prop, targetMeta, options.parentTableAlias, tableAlias, JoinType.leftJoin, targetPath, schema);
1276
+ // Select fields from each target table
1277
+ fields.push(...this.getFieldsForJoinedLoad(qb, targetMeta, {
1278
+ ...options,
1279
+ populate: hint.children,
1280
+ parentTableAlias: tableAlias,
1281
+ parentJoinPath: targetPath,
1282
+ }));
1283
+ }
1284
+ continue;
1285
+ }
1002
1286
  // ignore ref joins of known FKs unless it's a filter hint
1003
1287
  if (ref && !hint.filter && (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))) {
1004
1288
  continue;
1005
1289
  }
1006
- const meta2 = this.metadata.find(prop.type);
1290
+ const meta2 = prop.targetMeta;
1007
1291
  const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
1008
1292
  const tableAlias = qb.getNextAlias(prop.name);
1009
1293
  const field = `${options.parentTableAlias}.${prop.name}`;
@@ -1019,7 +1303,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
1019
1303
  : (hint.filter && !prop.nullable) || mandatoryToOneProperty
1020
1304
  ? JoinType.innerJoin
1021
1305
  : JoinType.leftJoin;
1022
- qb.join(field, tableAlias, {}, joinType, path);
1306
+ const schema = prop.targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : prop.targetMeta.schema;
1307
+ qb.join(field, tableAlias, {}, joinType, path, schema);
1308
+ // For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
1309
+ if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
1310
+ // Use the registry metadata to ensure allTPTDescendants is available
1311
+ const tptMeta = this.metadata.get(meta2.class);
1312
+ this.addTPTPolymorphicJoinsForRelation(qb, tptMeta, tableAlias, fields);
1313
+ }
1023
1314
  if (pivotRefJoin) {
1024
1315
  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
1316
  }
@@ -1040,7 +1331,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1040
1331
  }
1041
1332
  });
1042
1333
  const childExclude = options.exclude ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
1043
- if (!ref && !prop.mapToPk) {
1334
+ if (!ref && (!prop.mapToPk || hint.dataOnly)) {
1044
1335
  fields.push(...this.getFieldsForJoinedLoad(qb, meta2, {
1045
1336
  ...options,
1046
1337
  explicitFields: childExplicitFields.length === 0 ? undefined : childExplicitFields,
@@ -1050,23 +1341,127 @@ export class AbstractSqlDriver extends DatabaseDriver {
1050
1341
  parentJoinPath: path,
1051
1342
  }));
1052
1343
  }
1053
- else if (hint.filter || prop.mapToPk || (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
1344
+ else if (hint.filter || (prop.mapToPk && !hint.dataOnly) || (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
1054
1345
  fields.push(...prop.referencedColumnNames.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
1055
1346
  }
1056
1347
  }
1057
1348
  return fields;
1058
1349
  }
1059
1350
  /**
1351
+ * Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
1060
1352
  * @internal
1061
1353
  */
1062
- mapPropToFieldNames(qb, prop, tableAlias, explicitFields) {
1354
+ addTPTPolymorphicJoinsForRelation(qb, meta, baseAlias, fields) {
1355
+ // allTPTDescendants is pre-computed during discovery, sorted by depth (deepest first)
1356
+ const descendants = meta.allTPTDescendants;
1357
+ const childAliases = {};
1358
+ // LEFT JOIN each descendant table
1359
+ for (const childMeta of descendants) {
1360
+ const childAlias = qb.getNextAlias(childMeta.className);
1361
+ qb.createAlias(childMeta.class, childAlias);
1362
+ childAliases[childMeta.className] = childAlias;
1363
+ qb.addPropertyJoin(childMeta.tptInverseProp, baseAlias, childAlias, JoinType.leftJoin, `[tpt]${meta.className}`);
1364
+ // Add fields from this child (only ownProps, skip PKs)
1365
+ for (const prop of childMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
1366
+ for (const fieldName of prop.fieldNames) {
1367
+ const field = `${childAlias}.${fieldName}`;
1368
+ const fieldAlias = `${childAlias}__${fieldName}`;
1369
+ fields.push(raw(`${this.platform.quoteIdentifier(field)} as ${this.platform.quoteIdentifier(fieldAlias)}`));
1370
+ }
1371
+ }
1372
+ }
1373
+ // Add computed discriminator (descendants already sorted by depth)
1374
+ if (meta.root.tptDiscriminatorColumn) {
1375
+ fields.push(this.buildTPTDiscriminatorExpression(meta, descendants, childAliases, baseAlias));
1376
+ }
1377
+ }
1378
+ /**
1379
+ * Find the alias for a TPT child table in the query builder.
1380
+ * @internal
1381
+ */
1382
+ findTPTChildAlias(qb, childMeta) {
1383
+ const joins = qb._joins;
1384
+ for (const key of Object.keys(joins)) {
1385
+ if (joins[key].table === childMeta.tableName && key.includes('[tpt]')) {
1386
+ return joins[key].alias;
1387
+ }
1388
+ }
1389
+ return undefined;
1390
+ }
1391
+ /**
1392
+ * Builds a CASE WHEN expression for TPT discriminator.
1393
+ * Determines concrete entity type based on which child table has a non-null PK.
1394
+ * @internal
1395
+ */
1396
+ buildTPTDiscriminatorExpression(meta, descendants, aliasMap, baseAlias) {
1397
+ const cases = descendants.map(child => {
1398
+ const childAlias = aliasMap[child.className];
1399
+ const pkFieldName = child.properties[child.primaryKeys[0]].fieldNames[0];
1400
+ return `when ${this.platform.quoteIdentifier(`${childAlias}.${pkFieldName}`)} is not null then '${child.discriminatorValue}'`;
1401
+ });
1402
+ const defaultValue = meta.abstract ? 'null' : `'${meta.discriminatorValue}'`;
1403
+ const caseExpr = `case ${cases.join(' ')} else ${defaultValue} end`;
1404
+ const aliased = this.platform.quoteIdentifier(`${baseAlias}__${meta.root.tptDiscriminatorColumn}`);
1405
+ return raw(`${caseExpr} as ${aliased}`);
1406
+ }
1407
+ /**
1408
+ * Maps TPT child-specific fields during hydration.
1409
+ * When a relation points to a TPT base class, the actual entity might be a child class.
1410
+ * This method reads the discriminator to determine the concrete type and maps child-specific fields.
1411
+ * @internal
1412
+ */
1413
+ mapTPTChildFields(relationPojo, meta, relationAlias, qb, root) {
1414
+ // Check if this is a TPT base with polymorphic children
1415
+ if (meta.inheritanceType !== 'tpt' || !meta.root.tptDiscriminatorColumn) {
1416
+ return;
1417
+ }
1418
+ // Read the discriminator value
1419
+ const discriminatorAlias = `${relationAlias}__${meta.root.tptDiscriminatorColumn}`;
1420
+ const discriminatorValue = root[discriminatorAlias];
1421
+ if (!discriminatorValue) {
1422
+ return;
1423
+ }
1424
+ // Set the discriminator in the pojo for EntityFactory
1425
+ relationPojo[meta.root.tptDiscriminatorColumn] = discriminatorValue;
1426
+ // Find the concrete metadata from discriminator map
1427
+ const concreteClass = meta.root.discriminatorMap?.[discriminatorValue];
1428
+ /* v8 ignore next 3 - defensive check for invalid discriminator values */
1429
+ if (!concreteClass) {
1430
+ return;
1431
+ }
1432
+ const concreteMeta = this.metadata.get(concreteClass);
1433
+ if (concreteMeta === meta) {
1434
+ // Already the concrete type, no child fields to map
1435
+ delete root[discriminatorAlias];
1436
+ return;
1437
+ }
1438
+ // Traverse up from concrete type and map fields from each level's table
1439
+ const tz = this.platform.getTimezone();
1440
+ let currentMeta = concreteMeta;
1441
+ while (currentMeta && currentMeta !== meta) {
1442
+ const childAlias = this.findTPTChildAlias(qb, currentMeta);
1443
+ if (childAlias) {
1444
+ // Map fields using same filtering as joined loading, plus skip PKs
1445
+ for (const prop of currentMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
1446
+ this.mapJoinedProp(relationPojo, prop, childAlias, root, tz, currentMeta, { deleteFromRoot: true });
1447
+ }
1448
+ }
1449
+ currentMeta = currentMeta.tptParent;
1450
+ }
1451
+ // Clean up the discriminator alias
1452
+ delete root[discriminatorAlias];
1453
+ }
1454
+ /**
1455
+ * @internal
1456
+ */
1457
+ mapPropToFieldNames(qb, prop, tableAlias, meta, schema, explicitFields) {
1063
1458
  if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
1064
1459
  return Object.entries(prop.embeddedProps).flatMap(([name, childProp]) => {
1065
1460
  const childFields = explicitFields ? Utils.extractChildElements(explicitFields, prop.name) : [];
1066
1461
  if (!this.shouldHaveColumn(prop.targetMeta, { ...childProp, name }, [], childFields.length > 0 ? childFields : undefined)) {
1067
1462
  return [];
1068
1463
  }
1069
- return this.mapPropToFieldNames(qb, childProp, tableAlias, childFields);
1464
+ return this.mapPropToFieldNames(qb, childProp, tableAlias, meta, schema, childFields);
1070
1465
  });
1071
1466
  }
1072
1467
  const aliased = this.platform.quoteIdentifier(`${tableAlias}__${prop.fieldNames[0]}`);
@@ -1085,8 +1480,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
1085
1480
  return [raw(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
1086
1481
  }
1087
1482
  if (prop.formula) {
1088
- const alias = this.platform.quoteIdentifier(tableAlias);
1089
- return [raw(`${prop.formula(alias)} as ${aliased}`)];
1483
+ const quotedAlias = this.platform.quoteIdentifier(tableAlias).toString();
1484
+ const table = this.createFormulaTable(quotedAlias, meta, schema);
1485
+ const columns = meta.createColumnMappingObject(tableAlias);
1486
+ return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
1090
1487
  }
1091
1488
  return prop.fieldNames.map(fieldName => {
1092
1489
  return `${tableAlias}.${fieldName} as ${tableAlias}__${fieldName}`;
@@ -1114,26 +1511,20 @@ export class AbstractSqlDriver extends DatabaseDriver {
1114
1511
  }
1115
1512
  return 'write';
1116
1513
  }
1117
- extractManyToMany(entityName, data) {
1118
- if (!this.metadata.has(entityName)) {
1119
- return {};
1120
- }
1514
+ extractManyToMany(meta, data) {
1121
1515
  const ret = {};
1122
- this.metadata.find(entityName).relations.forEach(prop => {
1516
+ for (const prop of meta.relations) {
1123
1517
  if (prop.kind === ReferenceKind.MANY_TO_MANY && data[prop.name]) {
1124
1518
  ret[prop.name] = data[prop.name].map((item) => Utils.asArray(item));
1125
1519
  delete data[prop.name];
1126
1520
  }
1127
- });
1521
+ }
1128
1522
  return ret;
1129
1523
  }
1130
1524
  async processManyToMany(meta, pks, collections, clear, options) {
1131
- if (!meta) {
1132
- return;
1133
- }
1134
1525
  for (const prop of meta.relations) {
1135
1526
  if (collections[prop.name]) {
1136
- const pivotMeta = this.metadata.find(prop.pivotEntity);
1527
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
1137
1528
  const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
1138
1529
  persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
1139
1530
  await this.rethrow(persister.execute());
@@ -1142,9 +1533,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
1142
1533
  }
1143
1534
  async lockPessimistic(entity, options) {
1144
1535
  const meta = helper(entity).__meta;
1145
- const qb = this.createQueryBuilder(entity.constructor.name, options.ctx, undefined, undefined, options.logging).withSchema(options.schema ?? meta.schema);
1536
+ const qb = this.createQueryBuilder(meta.class, options.ctx, undefined, undefined, options.logging).withSchema(options.schema ?? meta.schema);
1146
1537
  const cond = Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
1147
- qb.select(raw('1')).where(cond).setLockMode(options.lockMode, options.lockTableAliases);
1538
+ qb.select(raw('1'))
1539
+ .where(cond)
1540
+ .setLockMode(options.lockMode, options.lockTableAliases);
1148
1541
  await this.rethrow(qb.execute());
1149
1542
  }
1150
1543
  buildPopulateWhere(meta, joinedProps, options) {
@@ -1152,21 +1545,24 @@ export class AbstractSqlDriver extends DatabaseDriver {
1152
1545
  for (const hint of joinedProps) {
1153
1546
  const [propName] = hint.field.split(':', 2);
1154
1547
  const prop = meta.properties[propName];
1155
- if (!Utils.isEmpty(prop.where)) {
1548
+ if (!Utils.isEmpty(prop.where) || RawQueryFragment.hasObjectFragments(prop.where)) {
1156
1549
  where[prop.name] = Utils.copy(prop.where);
1157
1550
  }
1158
1551
  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);
1552
+ const targetMeta = prop.targetMeta;
1553
+ if (targetMeta) {
1554
+ const inner = this.buildPopulateWhere(targetMeta, hint.children, {});
1555
+ if (!Utils.isEmpty(inner) || RawQueryFragment.hasObjectFragments(inner)) {
1556
+ where[prop.name] ??= {};
1557
+ Object.assign(where[prop.name], inner);
1558
+ }
1163
1559
  }
1164
1560
  }
1165
1561
  }
1166
- if (Utils.isEmpty(options.populateWhere)) {
1562
+ if (Utils.isEmpty(options.populateWhere) && !RawQueryFragment.hasObjectFragments(options.populateWhere)) {
1167
1563
  return where;
1168
1564
  }
1169
- if (Utils.isEmpty(where)) {
1565
+ if (Utils.isEmpty(where) && !RawQueryFragment.hasObjectFragments(where)) {
1170
1566
  return options.populateWhere;
1171
1567
  }
1172
1568
  /* v8 ignore next */
@@ -1178,31 +1574,32 @@ export class AbstractSqlDriver extends DatabaseDriver {
1178
1574
  // as `options.populateWhere` will be always recomputed to respect filters
1179
1575
  const populateWhereAll = options._populateWhere !== 'infer' && !Utils.isEmpty(options._populateWhere);
1180
1576
  const path = (populateWhereAll ? '[populate]' : '') + meta.className;
1577
+ const optionsOrderBy = Utils.asArray(options.orderBy);
1181
1578
  const populateOrderBy = this.buildPopulateOrderBy(qb, meta, Utils.asArray(options.populateOrderBy ?? options.orderBy), path, !!options.populateOrderBy);
1182
1579
  const joinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta, joinedProps, options, path);
1183
- return [...Utils.asArray(options.orderBy), ...populateOrderBy, ...joinedPropsOrderBy];
1580
+ return [...optionsOrderBy, ...populateOrderBy, ...joinedPropsOrderBy];
1184
1581
  }
1185
1582
  buildPopulateOrderBy(qb, meta, populateOrderBy, parentPath, explicit, parentAlias = qb.alias) {
1186
1583
  const orderBy = [];
1187
1584
  for (let i = 0; i < populateOrderBy.length; i++) {
1188
1585
  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] });
1586
+ for (const field of Utils.getObjectQueryKeys(orderHint)) {
1587
+ const childOrder = orderHint[field];
1588
+ if (RawQueryFragment.isKnownFragmentSymbol(field)) {
1589
+ const { sql, params } = RawQueryFragment.getKnownFragment(field);
1590
+ const key = raw(sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), parentAlias), params);
1591
+ orderBy.push({ [key]: childOrder });
1195
1592
  continue;
1196
1593
  }
1197
- const prop = meta.properties[propName];
1594
+ const prop = meta.properties[field];
1198
1595
  if (!prop) {
1199
- throw new Error(`Trying to order by not existing property ${meta.className}.${propName}`);
1596
+ throw new Error(`Trying to order by not existing property ${meta.className}.${field}`);
1200
1597
  }
1201
1598
  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}`;
1599
+ const meta2 = prop.targetMeta;
1600
+ if (prop.kind !== ReferenceKind.SCALAR &&
1601
+ (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || Utils.isPlainObject(childOrder))) {
1602
+ path += `.${field}`;
1206
1603
  }
1207
1604
  if (prop.kind === ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
1208
1605
  path += '[pivot]';
@@ -1228,9 +1625,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1228
1625
  }
1229
1626
  continue;
1230
1627
  }
1231
- const order = typeof childOrder === 'object' ? childOrder[propName] : childOrder;
1628
+ const order = typeof childOrder === 'object' ? childOrder[field] : childOrder;
1232
1629
  if (order) {
1233
- orderBy.push({ [`${propAlias}.${propName}`]: order });
1630
+ orderBy.push({ [`${propAlias}.${field}`]: order });
1234
1631
  }
1235
1632
  }
1236
1633
  }
@@ -1249,27 +1646,26 @@ export class AbstractSqlDriver extends DatabaseDriver {
1249
1646
  }
1250
1647
  const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
1251
1648
  const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
1252
- const meta2 = this.metadata.find(prop.type);
1253
1649
  if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
1254
1650
  const alias = ref ? propAlias : join.ownerAlias;
1255
1651
  orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC });
1256
1652
  }
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] });
1653
+ const effectiveOrderBy = QueryHelper.mergeOrderBy(propOrderBy, prop.targetMeta?.orderBy);
1654
+ for (const item of effectiveOrderBy) {
1655
+ for (const field of Utils.getObjectQueryKeys(item)) {
1656
+ const order = item[field];
1657
+ if (RawQueryFragment.isKnownFragmentSymbol(field)) {
1658
+ const { sql, params } = RawQueryFragment.getKnownFragment(field);
1659
+ const sql2 = propAlias ? sql.replace(new RegExp(ALIAS_REPLACEMENT_RE, 'g'), propAlias) : sql;
1660
+ const key = raw(sql2, params);
1661
+ orderBy.push({ [key]: order });
1662
+ continue;
1268
1663
  }
1664
+ orderBy.push({ [`${propAlias}.${field}`]: order });
1269
1665
  }
1270
1666
  }
1271
1667
  if (hint.children) {
1272
- const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta2, hint.children, options, path);
1668
+ const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, prop.targetMeta, hint.children, options, path);
1273
1669
  orderBy.push(...buildJoinedPropsOrderBy);
1274
1670
  }
1275
1671
  }
@@ -1313,7 +1709,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1313
1709
  }
1314
1710
  ret.push(prop.name);
1315
1711
  }
1316
- buildFields(meta, populate, joinedProps, qb, alias, options) {
1712
+ buildFields(meta, populate, joinedProps, qb, alias, options, schema) {
1317
1713
  const lazyProps = meta.props.filter(prop => prop.lazy && !populate.some(p => this.isPopulated(meta, prop, p)));
1318
1714
  const hasLazyFormulas = meta.props.some(p => p.lazy && p.formula);
1319
1715
  const requiresSQLConversion = meta.props.some(p => p.customType?.convertToJSValueSQL && p.persist !== false);
@@ -1332,7 +1728,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1332
1728
  const prop = QueryHelper.findProperty(rootPropName, {
1333
1729
  metadata: this.metadata,
1334
1730
  platform: this.platform,
1335
- entityName: meta.className,
1731
+ entityName: meta.class,
1336
1732
  where: {},
1337
1733
  aliasMap: qb.getAliasMap(),
1338
1734
  });
@@ -1341,7 +1737,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1341
1737
  if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
1342
1738
  ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
1343
1739
  }
1344
- if (meta.root.discriminatorColumn && !options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
1740
+ if (meta.root.inheritanceType === 'sti' && !options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
1345
1741
  ret.push(meta.root.discriminatorColumn);
1346
1742
  }
1347
1743
  }
@@ -1358,14 +1754,18 @@ export class AbstractSqlDriver extends DatabaseDriver {
1358
1754
  ret.push('*');
1359
1755
  }
1360
1756
  if (ret.length > 0 && !hasExplicitFields && addFormulas) {
1757
+ // Create formula column mapping with unquoted aliases - quoting should be handled by the user via `quote` helper
1758
+ const quotedAlias = this.platform.quoteIdentifier(alias);
1759
+ const columns = meta.createColumnMappingObject(alias);
1760
+ const effectiveSchema = schema ?? (meta.schema !== '*' ? meta.schema : undefined);
1361
1761
  for (const prop of meta.props) {
1362
1762
  if (lazyProps.includes(prop)) {
1363
1763
  continue;
1364
1764
  }
1365
1765
  if (prop.formula) {
1366
- const a = this.platform.quoteIdentifier(alias);
1367
1766
  const aliased = this.platform.quoteIdentifier(prop.fieldNames[0]);
1368
- ret.push(raw(`${prop.formula(a)} as ${aliased}`));
1767
+ const table = this.createFormulaTable(quotedAlias.toString(), meta, effectiveSchema);
1768
+ ret.push(raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`));
1369
1769
  }
1370
1770
  if (!prop.object && (prop.hasConvertToDatabaseValueSQL || prop.hasConvertToJSValueSQL)) {
1371
1771
  ret.push(prop.name);