@mikro-orm/sql 7.0.0-dev.224 → 7.0.0-dev.226

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.
@@ -26,6 +26,11 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
26
26
  protected wrapVirtualExpressionInSubqueryStream<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: FindOptions<T, any, any, any>, type: QueryType.SELECT): AsyncIterableIterator<T>;
27
27
  mapResult<T extends object>(result: EntityData<T>, meta: EntityMetadata<T>, populate?: PopulateOptions<T>[], qb?: QueryBuilder<T, any, any, any>, map?: Dictionary): EntityData<T> | null;
28
28
  private mapJoinedProps;
29
+ /**
30
+ * Maps a single property from a joined result row into the relation pojo.
31
+ * Handles polymorphic FKs, composite keys, Date parsing, and embedded objects.
32
+ */
33
+ private mapJoinedProp;
29
34
  count<T extends object>(entityName: EntityName<T>, where: any, options?: CountOptions<T>): Promise<number>;
30
35
  nativeInsert<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
31
36
  nativeInsertMany<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>;
@@ -40,6 +45,24 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
40
45
  private comparePrimaryKeyArrays;
41
46
  syncCollections<T extends object, O extends object>(collections: Iterable<Collection<T, O>>, options?: DriverMethodOptions): Promise<void>;
42
47
  loadFromPivotTable<T extends object, O extends object>(prop: EntityProperty, owners: Primary<O>[][], where?: FilterQuery<any>, orderBy?: OrderDefinition<T>, ctx?: Transaction, options?: FindOptions<T, any, any, any>, pivotJoin?: boolean): Promise<Dictionary<T[]>>;
48
+ /**
49
+ * Load from a polymorphic M:N pivot table.
50
+ */
51
+ protected loadFromPolymorphicPivotTable<T extends object, O extends object>(prop: EntityProperty, owners: Primary<O>[][], where?: FilterQuery<any>, orderBy?: OrderDefinition<T>, ctx?: Transaction, options?: FindOptions<T, any, any, any>, pivotJoin?: boolean): Promise<Dictionary<T[]>>;
52
+ /**
53
+ * Load from owner side of polymorphic M:N (e.g., Post -> Tags)
54
+ */
55
+ protected loadPolymorphicPivotOwnerSide<T extends object, O extends object>(prop: EntityProperty, owners: Primary<O>[][], where: FilterQuery<any>, orderBy?: OrderDefinition<T>, ctx?: Transaction, options?: FindOptions<T, any, any, any>, pivotJoin?: boolean, inverseProp?: EntityProperty): Promise<Dictionary<T[]>>;
56
+ /**
57
+ * Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
58
+ * Uses single query with join via virtual relation on pivot.
59
+ */
60
+ protected loadPolymorphicPivotInverseSide<T extends object, O extends object>(prop: EntityProperty, owners: Primary<O>[][], where: FilterQuery<any>, orderBy?: OrderDefinition<T>, ctx?: Transaction, options?: FindOptions<T, any, any, any>): Promise<Dictionary<T[]>>;
61
+ /**
62
+ * Build a map from owner PKs to their related entities from pivot table results.
63
+ */
64
+ private buildPivotResultMap;
65
+ private wrapPopulateFilter;
43
66
  private getPivotOrderBy;
44
67
  execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
45
68
  stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>): AsyncIterableIterator<T>;
@@ -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';
@@ -216,6 +216,43 @@ export class AbstractSqlDriver extends DatabaseDriver {
216
216
  if (!prop) {
217
217
  return;
218
218
  }
219
+ // Polymorphic to-one: iterate targets, find the matching one, build entity from its columns.
220
+ // Skip :ref hints — no JOINs were created, so the FK reference is already set by the result mapper.
221
+ if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
222
+ const basePath = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
223
+ const pathPrefix = !parentJoinPath ? '[populate]' : '';
224
+ let matched = false;
225
+ for (const targetMeta of prop.polymorphTargets) {
226
+ const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
227
+ const relationAlias = qb.getAliasForJoinPath(targetPath, { matchPopulateJoins: true });
228
+ const meta2 = targetMeta;
229
+ const targetProps = meta2.props.filter(p => this.platform.shouldHaveColumn(p, hint.children || []));
230
+ const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => root[`${relationAlias}__${name}`] != null));
231
+ if (hasPK && !matched) {
232
+ matched = true;
233
+ let relationPojo = {};
234
+ const tz = this.platform.getTimezone();
235
+ for (const p of targetProps) {
236
+ this.mapJoinedProp(relationPojo, p, relationAlias, root, tz, meta2);
237
+ }
238
+ // Inject the entity class constructor so that the factory creates the correct type
239
+ Object.defineProperty(relationPojo, 'constructor', { value: meta2.class, enumerable: false, configurable: true });
240
+ result[prop.name] = relationPojo;
241
+ const populateChildren = hint.children || [];
242
+ this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, targetPath);
243
+ }
244
+ // Clean up aliased columns for ALL targets (even non-matching ones)
245
+ for (const p of targetProps) {
246
+ for (const name of p.fieldNames) {
247
+ delete root[`${relationAlias}__${name}`];
248
+ }
249
+ }
250
+ }
251
+ if (!matched) {
252
+ result[prop.name] = null;
253
+ }
254
+ return;
255
+ }
219
256
  const pivotRefJoin = prop.kind === ReferenceKind.MANY_TO_MANY && ref;
220
257
  const meta2 = prop.targetMeta;
221
258
  let path = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
@@ -286,41 +323,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
286
323
  });
287
324
  const tz = this.platform.getTimezone();
288
325
  for (const prop of targetProps) {
289
- if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
290
- continue;
291
- }
292
- if (prop.fieldNames.length > 1) {
293
- // composite keys
294
- const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
295
- const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
296
- relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
297
- }
298
- else if (prop.runtimeType === 'Date') {
299
- const alias = `${relationAlias}__${prop.fieldNames[0]}`;
300
- const value = root[alias];
301
- if (tz && tz !== 'local' && typeof value === 'string' && !value.includes('+') && value.lastIndexOf('-') < 11 && !value.endsWith('Z')) {
302
- relationPojo[prop.name] = this.platform.parseDate(value + tz);
303
- }
304
- else if (['string', 'number'].includes(typeof value)) {
305
- relationPojo[prop.name] = this.platform.parseDate(value);
306
- }
307
- else {
308
- relationPojo[prop.name] = value;
309
- }
310
- }
311
- else {
312
- const alias = `${relationAlias}__${prop.fieldNames[0]}`;
313
- relationPojo[prop.name] = root[alias];
314
- if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
315
- const item = parseJsonSafe(relationPojo[prop.name]);
316
- if (Array.isArray(item)) {
317
- relationPojo[prop.name] = item.map(row => (row == null ? row : this.comparator.mapResult(prop.targetMeta, row)));
318
- }
319
- else {
320
- relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
321
- }
322
- }
323
- }
326
+ this.mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta2);
324
327
  }
325
328
  // properties can be mapped to multiple places, e.g. when sharing a column in multiple FKs,
326
329
  // so we need to delete them after everything is mapped from given level
@@ -345,6 +348,60 @@ export class AbstractSqlDriver extends DatabaseDriver {
345
348
  this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, path);
346
349
  });
347
350
  }
351
+ /**
352
+ * Maps a single property from a joined result row into the relation pojo.
353
+ * Handles polymorphic FKs, composite keys, Date parsing, and embedded objects.
354
+ */
355
+ mapJoinedProp(relationPojo, prop, relationAlias, root, tz, meta) {
356
+ if (prop.fieldNames.every(name => typeof root[`${relationAlias}__${name}`] === 'undefined')) {
357
+ return;
358
+ }
359
+ if (prop.polymorphic) {
360
+ const discriminatorAlias = `${relationAlias}__${prop.fieldNames[0]}`;
361
+ const discriminatorValue = root[discriminatorAlias];
362
+ const pkFieldNames = prop.fieldNames.slice(1);
363
+ const pkValues = pkFieldNames.map(name => root[`${relationAlias}__${name}`]);
364
+ const pkValue = pkValues.length === 1 ? pkValues[0] : pkValues;
365
+ if (discriminatorValue != null && pkValue != null) {
366
+ relationPojo[prop.name] = new PolymorphicRef(discriminatorValue, pkValue);
367
+ }
368
+ else {
369
+ relationPojo[prop.name] = null;
370
+ }
371
+ }
372
+ else if (prop.fieldNames.length > 1) {
373
+ // composite keys
374
+ const fk = prop.fieldNames.map(name => root[`${relationAlias}__${name}`]);
375
+ const pk = Utils.mapFlatCompositePrimaryKey(fk, prop);
376
+ relationPojo[prop.name] = pk.every(val => val != null) ? pk : null;
377
+ }
378
+ else if (prop.runtimeType === 'Date') {
379
+ const alias = `${relationAlias}__${prop.fieldNames[0]}`;
380
+ const value = root[alias];
381
+ if (tz && tz !== 'local' && typeof value === 'string' && !value.includes('+') && value.lastIndexOf('-') < 11 && !value.endsWith('Z')) {
382
+ relationPojo[prop.name] = this.platform.parseDate(value + tz);
383
+ }
384
+ else if (['string', 'number'].includes(typeof value)) {
385
+ relationPojo[prop.name] = this.platform.parseDate(value);
386
+ }
387
+ else {
388
+ relationPojo[prop.name] = value;
389
+ }
390
+ }
391
+ else {
392
+ const alias = `${relationAlias}__${prop.fieldNames[0]}`;
393
+ relationPojo[prop.name] = root[alias];
394
+ if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
395
+ const item = parseJsonSafe(relationPojo[prop.name]);
396
+ if (Array.isArray(item)) {
397
+ relationPojo[prop.name] = item.map(row => (row == null ? row : this.comparator.mapResult(prop.targetMeta, row)));
398
+ }
399
+ else {
400
+ relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
401
+ }
402
+ }
403
+ }
404
+ }
348
405
  async count(entityName, where, options = {}) {
349
406
  const meta = this.metadata.get(entityName);
350
407
  if (meta.virtual) {
@@ -471,7 +528,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
471
528
  }
472
529
  if (prop.fieldNames.length > 1) {
473
530
  const newFields = [];
474
- const rawParam = Utils.asArray(row[prop.name]) ?? prop.fieldNames.map(() => null);
531
+ let rawParam;
532
+ const target = row[prop.name];
533
+ if (prop.polymorphic && target instanceof PolymorphicRef) {
534
+ rawParam = target.toTuple();
535
+ }
536
+ else {
537
+ rawParam = Utils.asArray(target) ?? prop.fieldNames.map(() => null);
538
+ }
475
539
  // Deep flatten nested arrays when needed (for deeply nested composite keys like Tag -> Comment -> Post -> User)
476
540
  const needsFlatten = rawParam.length !== prop.fieldNames.length && rawParam.some(v => Array.isArray(v));
477
541
  const allParam = needsFlatten ? Utils.flatten(rawParam, true) : rawParam;
@@ -644,6 +708,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
644
708
  };
645
709
  for (const key of keys) {
646
710
  const prop = meta.properties[key] ?? meta.root.properties[key];
711
+ if (prop.polymorphic && prop.fieldNames.length > 1) {
712
+ for (let idx = 0; idx < data.length; idx++) {
713
+ const rowValue = data[idx][key];
714
+ if (rowValue instanceof PolymorphicRef) {
715
+ data[idx][key] = rowValue.toTuple();
716
+ }
717
+ }
718
+ }
647
719
  prop.fieldNames.forEach((fieldName, fieldNameIdx) => {
648
720
  if (fields.has(fieldName) || (prop.ownColumns && !prop.ownColumns.includes(fieldName))) {
649
721
  return;
@@ -814,10 +886,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
814
886
  }
815
887
  }
816
888
  async loadFromPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
889
+ /* v8 ignore next */
817
890
  if (owners.length === 0) {
818
891
  return {};
819
892
  }
820
893
  const pivotMeta = this.metadata.get(prop.pivotEntity);
894
+ if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
895
+ return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
896
+ }
821
897
  const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
822
898
  const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
823
899
  const ownerMeta = pivotProp2.targetMeta;
@@ -851,21 +927,122 @@ export class AbstractSqlDriver extends DatabaseDriver {
851
927
  populateWhere: undefined,
852
928
  // @ts-ignore
853
929
  _populateWhere: 'infer',
854
- populateFilter: !Utils.isEmpty(options?.populateFilter) || RawQueryFragment.hasObjectFragments(options?.populateFilter)
855
- ? { [pivotProp2.name]: options?.populateFilter }
856
- : undefined,
930
+ populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
931
+ });
932
+ return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
933
+ }
934
+ /**
935
+ * Load from a polymorphic M:N pivot table.
936
+ */
937
+ async loadFromPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options, pivotJoin) {
938
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
939
+ // Find the M:1 relation on the pivot pointing to the target entity.
940
+ // We exclude virtual polymorphic owner relations (persist: false) and non-M:1 relations.
941
+ const inverseProp = pivotMeta.relations.find(r => r.kind === ReferenceKind.MANY_TO_ONE && r.persist !== false && r.targetMeta === prop.targetMeta);
942
+ if (inverseProp) {
943
+ return this.loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp);
944
+ }
945
+ return this.loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options);
946
+ }
947
+ /**
948
+ * Load from owner side of polymorphic M:N (e.g., Post -> Tags)
949
+ */
950
+ async loadPolymorphicPivotOwnerSide(prop, owners, where, orderBy, ctx, options, pivotJoin, inverseProp) {
951
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
952
+ const targetMeta = prop.targetMeta;
953
+ // Build condition: discriminator = 'post' AND {discriminator} IN (...)
954
+ const cond = {
955
+ [prop.discriminatorColumn]: prop.discriminatorValue,
956
+ [prop.discriminator]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
957
+ };
958
+ if (!Utils.isEmpty(where)) {
959
+ cond[inverseProp.name] = { ...where };
960
+ }
961
+ const populateField = pivotJoin ? `${inverseProp.name}:ref` : inverseProp.name;
962
+ const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
963
+ const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${inverseProp.name}.${f}`) : [];
964
+ const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${inverseProp.name}.${f}`) : [];
965
+ const fields = pivotJoin
966
+ ? [inverseProp.name, prop.discriminator, prop.discriminatorColumn]
967
+ : [inverseProp.name, prop.discriminator, prop.discriminatorColumn, ...childFields];
968
+ const res = await this.find(pivotMeta.class, cond, {
969
+ ctx,
970
+ ...options,
971
+ fields,
972
+ exclude: childExclude,
973
+ orderBy: this.getPivotOrderBy(prop, inverseProp, orderBy, options?.orderBy),
974
+ populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate, dataOnly: inverseProp.mapToPk && !pivotJoin }],
975
+ populateWhere: undefined,
976
+ // @ts-ignore
977
+ _populateWhere: 'infer',
978
+ populateFilter: this.wrapPopulateFilter(options, inverseProp.name),
857
979
  });
980
+ return this.buildPivotResultMap(owners, res, prop.discriminator, inverseProp.name);
981
+ }
982
+ /**
983
+ * Load from inverse side of polymorphic M:N (e.g., Tag -> Posts)
984
+ * Uses single query with join via virtual relation on pivot.
985
+ */
986
+ async loadPolymorphicPivotInverseSide(prop, owners, where, orderBy, ctx, options) {
987
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
988
+ const targetMeta = prop.targetMeta;
989
+ // Find the relation to the entity we're starting from (e.g., Tag_inverse -> Tag)
990
+ // Exclude virtual polymorphic owner relations (persist: false) - we want the actual M:N inverse relation
991
+ const tagProp = pivotMeta.relations.find(r => r.persist !== false && r.targetMeta !== targetMeta);
992
+ // Find the virtual relation to the polymorphic owner (e.g., taggable_Post -> Post)
993
+ const ownerRelationName = `${prop.discriminator}_${targetMeta.tableName}`;
994
+ const ownerProp = pivotMeta.properties[ownerRelationName];
995
+ // Build condition: discriminator = 'post' AND Tag_inverse IN (tagIds)
996
+ const cond = {
997
+ [prop.discriminatorColumn]: prop.discriminatorValue,
998
+ [tagProp.name]: { $in: owners.length === 1 && owners[0].length === 1 ? owners.map(o => o[0]) : owners },
999
+ };
1000
+ if (!Utils.isEmpty(where)) {
1001
+ cond[ownerRelationName] = { ...where };
1002
+ }
1003
+ const populateField = ownerRelationName;
1004
+ const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
1005
+ const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${ownerRelationName}.${f}`) : [];
1006
+ const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${ownerRelationName}.${f}`) : [];
1007
+ const fields = [ownerRelationName, tagProp.name, prop.discriminatorColumn, ...childFields];
1008
+ const res = await this.find(pivotMeta.class, cond, {
1009
+ ctx,
1010
+ ...options,
1011
+ fields,
1012
+ exclude: childExclude,
1013
+ orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
1014
+ populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate }],
1015
+ populateWhere: undefined,
1016
+ // @ts-ignore
1017
+ _populateWhere: 'infer',
1018
+ populateFilter: this.wrapPopulateFilter(options, ownerRelationName),
1019
+ });
1020
+ return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
1021
+ }
1022
+ /**
1023
+ * Build a map from owner PKs to their related entities from pivot table results.
1024
+ */
1025
+ buildPivotResultMap(owners, results, keyProp, valueProp) {
858
1026
  const map = {};
859
1027
  for (const owner of owners) {
860
1028
  const key = Utils.getPrimaryKeyHash(owner);
861
1029
  map[key] = [];
862
1030
  }
863
- for (const item of res) {
864
- const key = Utils.getPrimaryKeyHash(Utils.asArray(item[pivotProp2.name]));
865
- map[key].push(item[pivotProp1.name]);
1031
+ for (const item of results) {
1032
+ const key = Utils.getPrimaryKeyHash(Utils.asArray(item[keyProp]));
1033
+ const entity = item[valueProp];
1034
+ if (map[key]) {
1035
+ map[key].push(entity);
1036
+ }
866
1037
  }
867
1038
  return map;
868
1039
  }
1040
+ wrapPopulateFilter(options, propName) {
1041
+ if (!Utils.isEmpty(options?.populateFilter) || RawQueryFragment.hasObjectFragments(options?.populateFilter)) {
1042
+ return { [propName]: options?.populateFilter };
1043
+ }
1044
+ return undefined;
1045
+ }
869
1046
  getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
870
1047
  if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
871
1048
  return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
@@ -1022,6 +1199,26 @@ export class AbstractSqlDriver extends DatabaseDriver {
1022
1199
  for (const hint of joinedProps) {
1023
1200
  const [propName, ref] = hint.field.split(':', 2);
1024
1201
  const prop = meta.properties[propName];
1202
+ // Polymorphic to-one: create a LEFT JOIN per target type
1203
+ // Skip :ref hints — polymorphic to-one already has FK + discriminator in the row
1204
+ if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
1205
+ const basePath = options.parentJoinPath ? `${options.parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
1206
+ const pathPrefix = !options.parentJoinPath && populateWhereAll && !basePath.startsWith('[populate]') ? '[populate]' : '';
1207
+ for (const targetMeta of prop.polymorphTargets) {
1208
+ const tableAlias = qb.getNextAlias(targetMeta.className);
1209
+ const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
1210
+ const schema = targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : targetMeta.schema;
1211
+ qb.addPolymorphicJoin(prop, targetMeta, options.parentTableAlias, tableAlias, JoinType.leftJoin, targetPath, schema);
1212
+ // Select fields from each target table
1213
+ fields.push(...this.getFieldsForJoinedLoad(qb, targetMeta, {
1214
+ ...options,
1215
+ populate: hint.children,
1216
+ parentTableAlias: tableAlias,
1217
+ parentJoinPath: targetPath,
1218
+ }));
1219
+ }
1220
+ continue;
1221
+ }
1025
1222
  // ignore ref joins of known FKs unless it's a filter hint
1026
1223
  if (ref && !hint.filter && (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))) {
1027
1224
  continue;
@@ -1186,10 +1383,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
1186
1383
  where[prop.name] = Utils.copy(prop.where);
1187
1384
  }
1188
1385
  if (hint.children) {
1189
- const inner = this.buildPopulateWhere(prop.targetMeta, hint.children, {});
1190
- if (!Utils.isEmpty(inner) || RawQueryFragment.hasObjectFragments(inner)) {
1191
- where[prop.name] ??= {};
1192
- Object.assign(where[prop.name], inner);
1386
+ const targetMeta = prop.targetMeta;
1387
+ if (targetMeta) {
1388
+ const inner = this.buildPopulateWhere(targetMeta, hint.children, {});
1389
+ if (!Utils.isEmpty(inner) || RawQueryFragment.hasObjectFragments(inner)) {
1390
+ where[prop.name] ??= {};
1391
+ Object.assign(where[prop.name], inner);
1392
+ }
1193
1393
  }
1194
1394
  }
1195
1395
  }
@@ -1280,7 +1480,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
1280
1480
  }
1281
1481
  const join = qb.getJoinForPath(path, { matchPopulateJoins: true });
1282
1482
  const propAlias = qb.getAliasForJoinPath(join ?? path, { matchPopulateJoins: true });
1283
- const meta2 = prop.targetMeta;
1284
1483
  if (prop.kind === ReferenceKind.MANY_TO_MANY && prop.fixedOrder && join) {
1285
1484
  const alias = ref ? propAlias : join.ownerAlias;
1286
1485
  orderBy.push({ [`${alias}.${prop.fixedOrderColumn}`]: QueryOrder.ASC });
@@ -1301,7 +1500,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1301
1500
  }
1302
1501
  }
1303
1502
  if (hint.children) {
1304
- const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, meta2, hint.children, options, path);
1503
+ const buildJoinedPropsOrderBy = this.buildJoinedPropsOrderBy(qb, prop.targetMeta, hint.children, options, path);
1305
1504
  orderBy.push(...buildJoinedPropsOrderBy);
1306
1505
  }
1307
1506
  }
@@ -17,6 +17,11 @@ export declare class PivotCollectionPersister<Entity extends object> {
17
17
  private enqueueUpsert;
18
18
  private createInsertStatement;
19
19
  private enqueueDelete;
20
+ /**
21
+ * Build the keys and data arrays for pivot table operations.
22
+ * Handles polymorphic M:N by prepending the discriminator column/value.
23
+ */
24
+ private buildPivotKeysAndData;
20
25
  private collectStatements;
21
26
  execute(): Promise<void>;
22
27
  }
@@ -83,27 +83,45 @@ export class PivotCollectionPersister {
83
83
  }
84
84
  }
85
85
  createInsertStatement(prop, fks, pks) {
86
- const data = prop.owner ? [...fks, ...pks] : [...pks, ...fks];
87
- const keys = prop.owner
88
- ? [...prop.inverseJoinColumns, ...prop.joinColumns]
89
- : [...prop.joinColumns, ...prop.inverseJoinColumns];
86
+ const { data, keys } = this.buildPivotKeysAndData(prop, fks, pks);
90
87
  return new InsertStatement(keys, data, this.order++);
91
88
  }
92
89
  enqueueDelete(prop, deleteDiff, pks) {
93
90
  if (deleteDiff === true) {
94
- const statement = new DeleteStatement(prop.joinColumns, pks);
91
+ const { data, keys } = this.buildPivotKeysAndData(prop, [], pks, true);
92
+ const statement = new DeleteStatement(keys, data);
95
93
  this.deletes.set(statement.getHash(), statement);
96
94
  return;
97
95
  }
98
96
  for (const fks of deleteDiff) {
99
- const data = prop.owner ? [...fks, ...pks] : [...pks, ...fks];
100
- const keys = prop.owner
101
- ? [...prop.inverseJoinColumns, ...prop.joinColumns]
102
- : [...prop.joinColumns, ...prop.inverseJoinColumns];
97
+ const { data, keys } = this.buildPivotKeysAndData(prop, fks, pks);
103
98
  const statement = new DeleteStatement(keys, data);
104
99
  this.deletes.set(statement.getHash(), statement);
105
100
  }
106
101
  }
102
+ /**
103
+ * Build the keys and data arrays for pivot table operations.
104
+ * Handles polymorphic M:N by prepending the discriminator column/value.
105
+ */
106
+ buildPivotKeysAndData(prop, fks, pks, deleteAll = false) {
107
+ let data;
108
+ let keys;
109
+ if (deleteAll) {
110
+ data = pks;
111
+ keys = prop.joinColumns;
112
+ }
113
+ else {
114
+ data = prop.owner ? [...fks, ...pks] : [...pks, ...fks];
115
+ keys = prop.owner
116
+ ? [...prop.inverseJoinColumns, ...prop.joinColumns]
117
+ : [...prop.joinColumns, ...prop.inverseJoinColumns];
118
+ }
119
+ if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
120
+ data = [prop.discriminatorValue, ...data];
121
+ keys = [prop.discriminatorColumn, ...keys];
122
+ }
123
+ return { data, keys };
124
+ }
107
125
  collectStatements(statements) {
108
126
  const items = [];
109
127
  for (const statement of statements.values()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.0.0-dev.224",
3
+ "version": "7.0.0-dev.226",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -56,6 +56,6 @@
56
56
  "@mikro-orm/core": "^6.6.4"
57
57
  },
58
58
  "peerDependencies": {
59
- "@mikro-orm/core": "7.0.0-dev.224"
59
+ "@mikro-orm/core": "7.0.0-dev.226"
60
60
  }
61
61
  }
@@ -662,6 +662,12 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
662
662
  * @internal
663
663
  */
664
664
  getNextAlias(entityName?: string | EntityName): string;
665
+ /**
666
+ * Registers a join for a specific polymorphic target type.
667
+ * Used by the driver to create per-target LEFT JOINs for JOINED loading.
668
+ * @internal
669
+ */
670
+ addPolymorphicJoin(prop: EntityProperty, targetMeta: EntityMetadata, ownerAlias: string, alias: string, type: JoinType, path: string, schema?: string): void;
665
671
  /**
666
672
  * @internal
667
673
  */
@@ -784,6 +784,21 @@ export class QueryBuilder {
784
784
  entityName = Utils.className(entityName);
785
785
  return this.driver.config.getNamingStrategy().aliasName(entityName, this.aliasCounter++);
786
786
  }
787
+ /**
788
+ * Registers a join for a specific polymorphic target type.
789
+ * Used by the driver to create per-target LEFT JOINs for JOINED loading.
790
+ * @internal
791
+ */
792
+ addPolymorphicJoin(prop, targetMeta, ownerAlias, alias, type, path, schema) {
793
+ // Override referencedColumnNames to use the specific target's PK columns
794
+ // (polymorphic targets may have different PK column names, e.g. org_id vs user_id)
795
+ const referencedColumnNames = targetMeta.getPrimaryProps().flatMap(pk => pk.fieldNames);
796
+ const targetProp = { ...prop, targetMeta, referencedColumnNames };
797
+ const aliasedName = `${ownerAlias}.${prop.name}[${targetMeta.className}]#${alias}`;
798
+ this._joins[aliasedName] = this.helper.joinManyToOneReference(targetProp, ownerAlias, alias, type, {}, schema);
799
+ this._joins[aliasedName].path = path;
800
+ this.createAlias(targetMeta.class, alias);
801
+ }
787
802
  /**
788
803
  * @internal
789
804
  */
@@ -1,4 +1,4 @@
1
- import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
1
+ import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, QueryHelper, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
2
2
  import { JoinType, QueryType } from './enums.js';
3
3
  import { NativeQueryBuilder } from './NativeQueryBuilder.js';
4
4
  /**
@@ -133,6 +133,14 @@ export class QueryBuilderHelper {
133
133
  const primaryKeys = prop.owner ? prop.joinColumns : prop2.referencedColumnNames;
134
134
  schema ??= prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta);
135
135
  cond = Utils.merge(cond, prop.where);
136
+ // For inverse side of polymorphic relations, add discriminator condition
137
+ if (!prop.owner && prop2.polymorphic && prop2.discriminatorColumn && prop2.discriminatorMap) {
138
+ const ownerMeta = this.aliasMap[ownerAlias]?.meta ?? this.metadata.get(this.entityName);
139
+ const discriminatorValue = QueryHelper.findDiscriminatorValue(prop2.discriminatorMap, ownerMeta.class);
140
+ if (discriminatorValue) {
141
+ cond[`${alias}.${prop2.discriminatorColumn}`] = discriminatorValue;
142
+ }
143
+ }
136
144
  return {
137
145
  prop, type, cond, ownerAlias, alias, table, schema,
138
146
  joinColumns, inverseJoinColumns, primaryKeys,
@@ -144,7 +152,9 @@ export class QueryBuilderHelper {
144
152
  table: this.getTableName(prop.targetMeta.class),
145
153
  schema: prop.targetMeta?.schema === '*' ? '*' : this.driver.getSchemaName(prop.targetMeta, { schema }),
146
154
  joinColumns: prop.referencedColumnNames,
147
- primaryKeys: prop.fieldNames,
155
+ // For polymorphic relations, fieldNames includes the discriminator column which is not
156
+ // part of the join condition - use joinColumns (the FK columns only) instead
157
+ primaryKeys: prop.polymorphic ? prop.joinColumns : prop.fieldNames,
148
158
  };
149
159
  }
150
160
  joinManyToManyReference(prop, ownerAlias, alias, pivotAlias, type, cond, path, schema) {
@@ -218,6 +228,15 @@ export class QueryBuilderHelper {
218
228
  const alias = join.inverseAlias ?? join.alias;
219
229
  join.cond[`${alias}.${typeProperty}`] = join.prop.targetMeta.discriminatorValue;
220
230
  }
231
+ // For polymorphic relations, add discriminator condition to filter by target entity type
232
+ if (join.prop.polymorphic && join.prop.discriminatorColumn && join.prop.discriminatorMap) {
233
+ const discriminatorValue = QueryHelper.findDiscriminatorValue(join.prop.discriminatorMap, join.prop.targetMeta.class);
234
+ if (discriminatorValue) {
235
+ const discriminatorCol = this.platform.quoteIdentifier(`${join.ownerAlias}.${join.prop.discriminatorColumn}`);
236
+ conditions.push(`${discriminatorCol} = ?`);
237
+ params.push(discriminatorValue);
238
+ }
239
+ }
221
240
  let sql = method + ' ';
222
241
  if (join.nested) {
223
242
  sql += `(${this.platform.quoteIdentifier(table)} as ${this.platform.quoteIdentifier(join.alias)}`;
@@ -103,7 +103,7 @@ export class DatabaseTable {
103
103
  const defaultValue = this.platform.getSchemaHelper().normalizeDefaultValue(prop.defaultRaw, prop.length);
104
104
  this.columns[field].default = defaultValue;
105
105
  });
106
- if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
106
+ if ([ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) && !prop.polymorphic) {
107
107
  const constraintName = this.getIndexName(prop.foreignKeyName ?? true, prop.fieldNames, 'foreign');
108
108
  let schema = prop.targetMeta.root.schema === '*' ? this.schema : (prop.targetMeta.root.schema ?? config.get('schema', this.platform.getDefaultSchemaName()));
109
109
  if (prop.referencedTableName.includes('.')) {
@@ -406,7 +406,10 @@ export class SqlSchemaGenerator extends AbstractSchemaGenerator {
406
406
  }
407
407
  return;
408
408
  }
409
- await Utils.runSerial(groups.flat(), line => this.driver.execute(line));
409
+ const statements = groups.flatMap(group => {
410
+ return group.join('\n').split(';\n').map(s => s.trim()).filter(s => s);
411
+ });
412
+ await Utils.runSerial(statements, stmt => this.driver.execute(stmt));
410
413
  }
411
414
  async dropTableIfExists(name, schema) {
412
415
  const sql = this.helper.dropTableIfExists(name, schema);