@mikro-orm/sql 7.1.0-dev.5 → 7.1.0-dev.7

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.
@@ -83,11 +83,22 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
83
83
  * Uses single query with join via virtual relation on pivot.
84
84
  */
85
85
  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[]>>;
86
+ /**
87
+ * Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
88
+ * Each pivot row's discriminator column selects which target table to hydrate.
89
+ */
90
+ protected loadFromUnionTargetPolymorphicPivotTable<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[]>>;
86
91
  /**
87
92
  * Build a map from owner PKs to their related entities from pivot table results.
88
93
  */
89
94
  private buildPivotResultMap;
90
95
  private wrapPopulateFilter;
96
+ /**
97
+ * The pivot query builder doesn't convert custom types — manually convert owner PKs to the DB
98
+ * representation. Returns `needsConversion` + `pkProp` so the caller can convert result FKs back
99
+ * to JS format for consistent key hashing in `buildPivotResultMap`.
100
+ */
101
+ private convertOwnerPksForPivotQuery;
91
102
  private getPivotOrderBy;
92
103
  execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
93
104
  stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>): AsyncIterableIterator<T>;
@@ -34,6 +34,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
34
34
  return { alias, name: meta.tableName, schema: effectiveSchema, qualifiedName, toString: () => alias };
35
35
  }
36
36
  validateSqlOptions(options) {
37
+ if (options.using && !options.indexHint) {
38
+ const names = Utils.asArray(options.using);
39
+ const hint = this.platform.formatIndexHint(names);
40
+ if (hint) {
41
+ options.indexHint = hint;
42
+ }
43
+ }
37
44
  if (options.collation != null && typeof options.collation !== 'string') {
38
45
  throw new Error('Collation option for SQL drivers must be a string (collation name). Use a CollationOptions object only with MongoDB.');
39
46
  }
@@ -1099,8 +1106,29 @@ export class AbstractSqlDriver extends DatabaseDriver {
1099
1106
  const pks = wrapped.getPrimaryKeys(true);
1100
1107
  const snap = coll.getSnapshot();
1101
1108
  const includes = (arr, item) => !!arr.find(i => this.comparePrimaryKeyArrays(i, item));
1102
- const snapshot = snap ? snap.map(item => helper(item).getPrimaryKeys(true)) : [];
1103
- const current = coll.getItems(false).map(item => helper(item).getPrimaryKeys(true));
1109
+ // For union-target polymorphic M:N, prepend the per-row discriminator value so the pivot
1110
+ // persister can write it alongside the FK id. Memoized per sync-run because a collection can
1111
+ // hold hundreds of items of the same few types, and findDiscriminatorValue walks the prototype
1112
+ // chain + re-scans Object.entries each call.
1113
+ const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(coll.property);
1114
+ const classToDisc = new Map();
1115
+ const toDiff = (item) => {
1116
+ const keys = helper(item).getPrimaryKeys(true);
1117
+ if (!isUnionTargetMN) {
1118
+ return keys;
1119
+ }
1120
+ let disc = classToDisc.get(item.constructor);
1121
+ if (!classToDisc.has(item.constructor)) {
1122
+ disc = QueryHelper.findDiscriminatorValue(coll.property.discriminatorMap, item.constructor);
1123
+ if (disc === undefined) {
1124
+ throw new Error(`Cannot resolve discriminator value for ${item.constructor.name} in ${coll.property.name}; the class is not part of the union target list.`);
1125
+ }
1126
+ classToDisc.set(item.constructor, disc);
1127
+ }
1128
+ return [disc, ...keys];
1129
+ };
1130
+ const snapshot = snap ? snap.map(toDiff) : [];
1131
+ const current = coll.getItems(false).map(toDiff);
1104
1132
  const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
1105
1133
  const insertDiff = current.filter(item => !includes(snapshot, item));
1106
1134
  const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
@@ -1172,21 +1200,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
1172
1200
  return {};
1173
1201
  }
1174
1202
  const pivotMeta = this.metadata.get(prop.pivotEntity);
1203
+ if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
1204
+ return this.loadFromUnionTargetPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
1205
+ }
1175
1206
  if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
1176
1207
  return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
1177
1208
  }
1178
1209
  const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
1179
1210
  const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
1180
1211
  const ownerMeta = pivotProp2.targetMeta;
1181
- // The pivot query builder doesn't convert custom types, so we need to manually
1182
- // convert owner PKs to DB format for the query and convert result FKs back to
1183
- // JS format for consistent key hashing in buildPivotResultMap.
1184
- const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
1185
- const needsConversion = pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
1186
- let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
1187
- if (needsConversion) {
1188
- ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
1189
- }
1212
+ const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
1190
1213
  const cond = {
1191
1214
  [pivotProp2.name]: { $in: ownerPks },
1192
1215
  };
@@ -1340,6 +1363,102 @@ export class AbstractSqlDriver extends DatabaseDriver {
1340
1363
  });
1341
1364
  return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
1342
1365
  }
1366
+ /**
1367
+ * Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
1368
+ * Each pivot row's discriminator column selects which target table to hydrate.
1369
+ */
1370
+ async loadFromUnionTargetPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options = {}, pivotJoin) {
1371
+ // :ref hints cannot be honored for union-target — EntityLoader.getReference needs a concrete
1372
+ // class per item, but the ref-mode map only carries flat PK values and would hydrate every
1373
+ // row as the first polymorph target. Fail loudly instead of silently corrupting the collection.
1374
+ if (pivotJoin) {
1375
+ throw new Error(`The ':ref' populate hint is not supported for union-target polymorphic M:N on ${prop.name}. Use a regular populate hint instead.`);
1376
+ }
1377
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
1378
+ const targets = prop.polymorphTargets;
1379
+ const ownerProp = pivotMeta.relations.find(r => r.persist !== false && !r.polymorphic);
1380
+ const discriminatorColumn = prop.discriminatorColumn;
1381
+ const ownerMeta = ownerProp.targetMeta;
1382
+ const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
1383
+ const pivotRows = (await this.find(pivotMeta.class, { [ownerProp.name]: { $in: ownerPks } }, {
1384
+ ctx,
1385
+ orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
1386
+ fields: [ownerProp.name, discriminatorColumn, prop.discriminator],
1387
+ populateWhere: undefined,
1388
+ // @ts-ignore
1389
+ _populateWhere: 'infer',
1390
+ }));
1391
+ /* v8 ignore next 7 - custom-type PK conversion, tested via loadFromPivotTable path */
1392
+ if (needsConversion) {
1393
+ for (const item of pivotRows) {
1394
+ const fk = item[ownerProp.name];
1395
+ if (fk != null) {
1396
+ item[ownerProp.name] = pkProp.customType.convertToJSValue(fk, this.platform);
1397
+ }
1398
+ }
1399
+ }
1400
+ const classMeta = new Map(targets.map(t => [t.class, t]));
1401
+ const rowsByTarget = new Map();
1402
+ for (const row of pivotRows) {
1403
+ const discValue = row[discriminatorColumn];
1404
+ const targetClass = prop.discriminatorMap[discValue];
1405
+ const targetMeta = classMeta.get(targetClass);
1406
+ /* v8 ignore next 3 - defensive: unknown discriminator value */
1407
+ if (!targetMeta) {
1408
+ continue;
1409
+ }
1410
+ const list = rowsByTarget.get(targetMeta) ?? [];
1411
+ list.push(row);
1412
+ rowsByTarget.set(targetMeta, list);
1413
+ }
1414
+ // Strip the outer find's orderBy/fields/exclude before bulk-loading targets by PK — those apply
1415
+ // to the owner query, not each polymorph target (Image and Video wouldn't share an orderBy field).
1416
+ // populateFilter is a filter on the populated collection; since union-target splits the pivot
1417
+ // and target queries, we merge it into the target-level `where` instead of wrapping it on the
1418
+ // pivot query (where joins to target tables aren't available).
1419
+ // Hoisted above the loop since `options` doesn't change per target.
1420
+ const { orderBy: _o, fields: _f, exclude: _e, populateFilter, ...childOptions } = options;
1421
+ const populate = options.populate ?? [];
1422
+ const orphanedRows = new Set();
1423
+ for (const [targetMeta, rows] of rowsByTarget) {
1424
+ const targetIds = rows.map(r => r[prop.discriminator]);
1425
+ // Union-target pivot stores one scalar FK per row; composite-PK targets are rejected at
1426
+ // metadata validation time, so a single primary key column is guaranteed here.
1427
+ const pkCol = targetMeta.primaryKeys[0];
1428
+ let cond = { [pkCol]: { $in: targetIds } };
1429
+ if (!Utils.isEmpty(where)) {
1430
+ cond = { $and: [cond, where] };
1431
+ }
1432
+ if (!Utils.isEmpty(populateFilter)) {
1433
+ cond = { $and: [cond, populateFilter] };
1434
+ }
1435
+ const results = (await this.find(targetMeta.class, cond, {
1436
+ ctx,
1437
+ ...childOptions,
1438
+ populate: populate,
1439
+ }));
1440
+ const byPk = new Map();
1441
+ for (const row of results) {
1442
+ Object.defineProperty(row, 'constructor', {
1443
+ value: targetMeta.class,
1444
+ enumerable: false,
1445
+ configurable: true,
1446
+ });
1447
+ byPk.set(Utils.getPrimaryKeyHash([row[pkCol]]), row);
1448
+ }
1449
+ for (const row of rows) {
1450
+ const pkHash = Utils.getPrimaryKeyHash([row[prop.discriminator]]);
1451
+ const entity = byPk.get(pkHash);
1452
+ if (entity == null) {
1453
+ orphanedRows.add(row);
1454
+ continue;
1455
+ }
1456
+ row[prop.discriminator] = entity;
1457
+ }
1458
+ }
1459
+ const result = orphanedRows.size > 0 ? pivotRows.filter(r => !orphanedRows.has(r)) : pivotRows;
1460
+ return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator);
1461
+ }
1343
1462
  /**
1344
1463
  * Build a map from owner PKs to their related entities from pivot table results.
1345
1464
  */
@@ -1364,6 +1483,21 @@ export class AbstractSqlDriver extends DatabaseDriver {
1364
1483
  }
1365
1484
  return undefined;
1366
1485
  }
1486
+ /**
1487
+ * The pivot query builder doesn't convert custom types — manually convert owner PKs to the DB
1488
+ * representation. Returns `needsConversion` + `pkProp` so the caller can convert result FKs back
1489
+ * to JS format for consistent key hashing in `buildPivotResultMap`.
1490
+ */
1491
+ convertOwnerPksForPivotQuery(owners, ownerMeta) {
1492
+ const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
1493
+ const needsConversion = !!pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
1494
+ let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
1495
+ /* v8 ignore next 4 - custom-type PK conversion, tested via loadFromPivotTable path */
1496
+ if (needsConversion) {
1497
+ ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
1498
+ }
1499
+ return { ownerPks, needsConversion, pkProp };
1500
+ }
1367
1501
  getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
1368
1502
  if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
1369
1503
  return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
@@ -1430,6 +1564,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
1430
1564
  if (ref && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
1431
1565
  return true;
1432
1566
  }
1567
+ // Union-target polymorphic M:N cannot be loaded via a single JOIN because rows span multiple
1568
+ // target tables; fall through to SELECT_IN which dispatches through `loadFromPivotTable`.
1569
+ // Polymorphic M:1 (to-one with target_type discriminator) is handled via LEFT JOINs elsewhere.
1570
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && QueryHelper.isUnionTargetPolymorphic(prop) && prop.owner) {
1571
+ return false;
1572
+ }
1433
1573
  // skip redundant joins for 1:1 owner population hints when using `mapToPk`
1434
1574
  if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
1435
1575
  return false;
@@ -1,3 +1,4 @@
1
+ import { QueryHelper, } from '@mikro-orm/core';
1
2
  class InsertStatement {
2
3
  order;
3
4
  #keys;
@@ -106,6 +107,16 @@ export class PivotCollectionPersister {
106
107
  buildPivotKeysAndData(prop, fks, pks, deleteAll = false) {
107
108
  let data;
108
109
  let keys;
110
+ // Union-target polymorphic M:N prepends the per-row discriminator to `fks` in syncCollections;
111
+ // Rails-style polymorphic M:N uses a static discriminatorValue on the prop. Normalize to a single
112
+ // "current row's discriminator" value so the prepend block below handles both cases uniformly.
113
+ let rowDiscriminator;
114
+ if (QueryHelper.isUnionTargetPolymorphic(prop) && !deleteAll && fks.length > 0) {
115
+ [rowDiscriminator, ...fks] = fks;
116
+ }
117
+ else if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
118
+ rowDiscriminator = prop.discriminatorValue;
119
+ }
109
120
  if (deleteAll) {
110
121
  data = pks;
111
122
  keys = prop.joinColumns;
@@ -116,8 +127,8 @@ export class PivotCollectionPersister {
116
127
  ? [...prop.inverseJoinColumns, ...prop.joinColumns]
117
128
  : [...prop.joinColumns, ...prop.inverseJoinColumns];
118
129
  }
119
- if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
120
- data = [prop.discriminatorValue, ...data];
130
+ if (rowDiscriminator !== undefined) {
131
+ data = [rowDiscriminator, ...data];
121
132
  keys = [prop.discriminatorColumn, ...keys];
122
133
  }
123
134
  return { data, keys };
@@ -17,6 +17,7 @@ export declare class BaseMySqlPlatform extends AbstractSqlPlatform {
17
17
  supportsMultiColumnCountDistinct(): boolean;
18
18
  /** @internal */
19
19
  createNativeQueryBuilder(): MySqlNativeQueryBuilder;
20
+ formatIndexHint(indexNames: string[]): string;
20
21
  getDefaultCharset(): string;
21
22
  init(orm: MikroORM): void;
22
23
  getBeginTransactionSQL(options?: {
@@ -25,6 +25,9 @@ export class BaseMySqlPlatform extends AbstractSqlPlatform {
25
25
  createNativeQueryBuilder() {
26
26
  return new MySqlNativeQueryBuilder(this);
27
27
  }
28
+ formatIndexHint(indexNames) {
29
+ return `use index(${indexNames.join(', ')})`;
30
+ }
28
31
  getDefaultCharset() {
29
32
  return 'utf8mb4';
30
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.1.0-dev.5",
3
+ "version": "7.1.0-dev.7",
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
  "keywords": [
6
6
  "data-mapper",
@@ -53,7 +53,7 @@
53
53
  "@mikro-orm/core": "^7.0.11"
54
54
  },
55
55
  "peerDependencies": {
56
- "@mikro-orm/core": "7.1.0-dev.5"
56
+ "@mikro-orm/core": "7.1.0-dev.7"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">= 22.17.0"