@mikro-orm/sql 7.1.0-dev.2 → 7.1.0-dev.20

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 (40) hide show
  1. package/AbstractSqlConnection.d.ts +1 -1
  2. package/AbstractSqlConnection.js +2 -2
  3. package/AbstractSqlDriver.d.ts +25 -1
  4. package/AbstractSqlDriver.js +315 -15
  5. package/PivotCollectionPersister.js +13 -2
  6. package/SqlEntityManager.d.ts +5 -1
  7. package/SqlEntityManager.js +36 -1
  8. package/dialects/mssql/MsSqlNativeQueryBuilder.js +4 -0
  9. package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
  10. package/dialects/mysql/BaseMySqlPlatform.js +3 -0
  11. package/dialects/mysql/MySqlNativeQueryBuilder.js +11 -0
  12. package/dialects/mysql/MySqlSchemaHelper.d.ts +9 -3
  13. package/dialects/mysql/MySqlSchemaHelper.js +102 -4
  14. package/dialects/oracledb/OracleDialect.d.ts +1 -1
  15. package/dialects/oracledb/OracleDialect.js +2 -1
  16. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +1 -0
  17. package/dialects/postgresql/BasePostgreSqlPlatform.js +3 -0
  18. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +21 -1
  19. package/dialects/postgresql/PostgreSqlSchemaHelper.js +200 -4
  20. package/dialects/sqlite/SqliteSchemaHelper.d.ts +6 -1
  21. package/dialects/sqlite/SqliteSchemaHelper.js +114 -2
  22. package/package.json +3 -3
  23. package/query/CriteriaNode.d.ts +1 -1
  24. package/query/CriteriaNode.js +2 -2
  25. package/query/NativeQueryBuilder.d.ts +6 -0
  26. package/query/NativeQueryBuilder.js +16 -1
  27. package/query/ObjectCriteriaNode.js +1 -1
  28. package/query/QueryBuilder.d.ts +77 -0
  29. package/query/QueryBuilder.js +170 -6
  30. package/schema/DatabaseSchema.js +18 -0
  31. package/schema/DatabaseTable.d.ts +13 -1
  32. package/schema/DatabaseTable.js +50 -3
  33. package/schema/SchemaComparator.d.ts +1 -0
  34. package/schema/SchemaComparator.js +86 -1
  35. package/schema/SchemaHelper.d.ts +51 -1
  36. package/schema/SchemaHelper.js +191 -2
  37. package/schema/SqlSchemaGenerator.js +7 -0
  38. package/schema/partitioning.d.ts +13 -0
  39. package/schema/partitioning.js +326 -0
  40. package/typings.d.ts +32 -1
@@ -60,7 +60,7 @@ export declare abstract class AbstractSqlConnection extends Connection {
60
60
  /** Executes a SQL query and returns the result based on the method: `'all'` for rows, `'get'` for single row, `'run'` for affected count. */
61
61
  execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], method?: 'all' | 'get' | 'run', ctx?: Transaction, loggerContext?: LoggingOptions): Promise<T>;
62
62
  /** Executes a SQL query and returns an async iterable that yields results row by row. */
63
- stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions): AsyncIterableIterator<T>;
63
+ stream<T extends EntityData<AnyEntity>>(query: string | NativeQueryBuilder | RawQueryFragment, params?: readonly unknown[], ctx?: Transaction<Kysely<any>>, loggerContext?: LoggingOptions, chunkSize?: number): AsyncIterableIterator<T>;
64
64
  /** @inheritDoc */
65
65
  executeDump(dump: string): Promise<void>;
66
66
  protected getSql(query: string, formatted: string, context?: LogContext): string;
@@ -190,7 +190,7 @@ export class AbstractSqlConnection extends Connection {
190
190
  }, { ...q, ...loggerContext });
191
191
  }
192
192
  /** Executes a SQL query and returns an async iterable that yields results row by row. */
193
- async *stream(query, params = [], ctx, loggerContext) {
193
+ async *stream(query, params = [], ctx, loggerContext, chunkSize) {
194
194
  await this.ensureConnection();
195
195
  const q = this.prepareQuery(query, params);
196
196
  const sql = this.getSql(q.query, q.formatted, loggerContext);
@@ -203,7 +203,7 @@ export class AbstractSqlConnection extends Connection {
203
203
  parameters: [],
204
204
  };
205
205
  try {
206
- const res = (ctx ?? this.getClient()).getExecutor().stream(compiled, 1);
206
+ const res = (ctx ?? this.getClient()).getExecutor().stream(compiled, chunkSize ?? 100);
207
207
  this.logQuery(sql, {
208
208
  sql,
209
209
  params,
@@ -31,7 +31,7 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
31
31
  protected findFromVirtual<T extends object>(entityName: EntityName<T>, where: ObjectQuery<T>, options: FindOptions<T, any> | CountOptions<T, any>, type: QueryType): Promise<EntityData<T>[] | number>;
32
32
  protected streamFromVirtual<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any>): AsyncIterableIterator<EntityData<T>>;
33
33
  protected wrapVirtualExpressionInSubquery<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: FindOptions<T, any>, type: QueryType): Promise<T[] | number>;
34
- protected wrapVirtualExpressionInSubqueryStream<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: FindOptions<T, any, any, any>, type: QueryType.SELECT): AsyncIterableIterator<T>;
34
+ protected wrapVirtualExpressionInSubqueryStream<T extends object>(meta: EntityMetadata<T>, expression: string, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>, type: QueryType.SELECT): AsyncIterableIterator<T>;
35
35
  /**
36
36
  * Virtual entities have no PKs, so to-many populate joins can't be deduplicated.
37
37
  * Force balanced strategy to load to-many relations via separate queries.
@@ -52,6 +52,12 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
52
52
  private mapJoinedProp;
53
53
  count<T extends object>(entityName: EntityName<T>, where: any, options?: CountOptions<T>): Promise<number>;
54
54
  nativeInsert<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
55
+ nativeClone<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, overrides?: EntityData<T>, options?: NativeInsertUpdateOptions<T>): Promise<QueryResult<T>>;
56
+ private nativeCloneSimple;
57
+ private nativeCloneTPT;
58
+ private mapCloneOverrides;
59
+ private buildCloneFields;
60
+ private getCloneableProps;
55
61
  nativeInsertMany<T extends object>(entityName: EntityName<T>, data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T>, transform?: (sql: string) => string): Promise<QueryResult<T>>;
56
62
  nativeUpdate<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, data: EntityDictionary<T>, options?: NativeInsertUpdateOptions<T> & UpsertOptions<T>): Promise<QueryResult<T>>;
57
63
  nativeUpdateMany<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>[], data: EntityDictionary<T>[], options?: NativeInsertUpdateManyOptions<T> & UpsertManyOptions<T>, transform?: (sql: string, params: any[]) => string): Promise<QueryResult<T>>;
@@ -77,11 +83,22 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
77
83
  * Uses single query with join via virtual relation on pivot.
78
84
  */
79
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[]>>;
80
91
  /**
81
92
  * Build a map from owner PKs to their related entities from pivot table results.
82
93
  */
83
94
  private buildPivotResultMap;
84
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;
85
102
  private getPivotOrderBy;
86
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>;
87
104
  stream<T extends object>(entityName: EntityName<T>, where: FilterQuery<T>, options: StreamOptions<T, any, any, any>): AsyncIterableIterator<T>;
@@ -130,6 +147,13 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
130
147
  mapPropToFieldNames<T extends object>(qb: AnyQueryBuilder<T>, prop: EntityProperty<T>, tableAlias: string, meta: EntityMetadata<T>, schema?: string, explicitFields?: readonly InternalField<T>[]): InternalField<T>[];
131
148
  /** @internal */
132
149
  createQueryBuilder<T extends object>(entityName: EntityName<T> | AnyQueryBuilder<T>, ctx?: Transaction, preferredConnectionType?: ConnectionType, convertCustomTypes?: boolean, loggerContext?: LoggingOptions, alias?: string, em?: SqlEntityManager): AnyQueryBuilder<T>;
150
+ /**
151
+ * Renders a `FilterQuery` predicate into a SQL fragment (without the `WHERE` keyword and
152
+ * without table-alias prefixes) suitable for inlining into a partial-index DDL statement.
153
+ * Used by `DatabaseTable.addIndex` when the user passes an object `where` on `@Index` /
154
+ * `@Unique`. Strings are returned unchanged.
155
+ */
156
+ renderPartialIndexWhere<T extends object>(entityName: EntityName<T>, where: string | FilterQuery<T>): string;
133
157
  protected resolveConnectionType(args: {
134
158
  ctx?: Transaction;
135
159
  connectionType?: ConnectionType;
@@ -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
  }
@@ -88,6 +95,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
88
95
  if (options.em) {
89
96
  await qb.applyJoinedFilters(options.em, options.filters);
90
97
  }
98
+ if (options._partitionLimit) {
99
+ qb.setPartitionLimit(options._partitionLimit);
100
+ }
91
101
  return qb;
92
102
  }
93
103
  async find(entityName, where, options = {}) {
@@ -221,7 +231,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
221
231
  native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
222
232
  const query = native.compile();
223
233
  const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
224
- const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, options.loggerContext);
234
+ const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, options.loggerContext, options.chunkSize);
225
235
  for await (const row of res) {
226
236
  yield this.mapResult(row, meta);
227
237
  }
@@ -574,6 +584,109 @@ export class AbstractSqlDriver extends DatabaseDriver {
574
584
  await this.processManyToMany(meta, pk, collections, false, options);
575
585
  return res;
576
586
  }
587
+ async nativeClone(entityName, where, overrides, options = {}) {
588
+ options.convertCustomTypes ??= true;
589
+ const meta = this.metadata.get(entityName);
590
+ if (meta.inheritanceType === 'tpt' || meta.tptParent) {
591
+ return this.nativeCloneTPT(meta, where, overrides, options);
592
+ }
593
+ return this.nativeCloneSimple(meta, where, overrides, options);
594
+ }
595
+ async nativeCloneSimple(meta, where, overrides, options = {}) {
596
+ const props = this.getCloneableProps(meta);
597
+ const mappedOverrides = this.mapCloneOverrides(overrides, meta, options);
598
+ const { selectFields, insertColumns } = this.buildCloneFields(props, mappedOverrides, meta);
599
+ const selectQb = this.createQueryBuilder(meta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
600
+ selectQb.select(selectFields).where(where);
601
+ const insertQb = this.createQueryBuilder(meta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
602
+ return this.rethrow(insertQb
603
+ .insertFrom(selectQb, { columns: insertColumns })
604
+ .execute('run', false));
605
+ }
606
+ async nativeCloneTPT(leafMeta, where, overrides, options = {}) {
607
+ const hierarchy = [];
608
+ let current = leafMeta;
609
+ while (current) {
610
+ hierarchy.unshift(current);
611
+ current = current.tptParent;
612
+ }
613
+ const rootMeta = hierarchy[0];
614
+ let newPk;
615
+ let rootResult;
616
+ for (const tableMeta of hierarchy) {
617
+ const props = this.getCloneableProps(tableMeta, true);
618
+ const mappedOverrides = this.mapCloneOverrides(overrides, tableMeta, options);
619
+ const { selectFields, insertColumns } = this.buildCloneFields(props, mappedOverrides, tableMeta);
620
+ // For child tables, prepend the new PK value
621
+ if (tableMeta !== rootMeta && newPk != null) {
622
+ for (const pkName of tableMeta.primaryKeys) {
623
+ const prop = tableMeta.properties[pkName];
624
+ for (const fieldName of prop.fieldNames) {
625
+ insertColumns.unshift(fieldName);
626
+ selectFields.unshift(raw('? as ??', [newPk, fieldName]));
627
+ }
628
+ }
629
+ }
630
+ const sourceWhere = tableMeta === rootMeta ? where : (Utils.extractPK(where, tableMeta) ?? where);
631
+ const selectQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
632
+ selectQb.select(selectFields).where(sourceWhere);
633
+ const insertQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
634
+ const res = await this.rethrow(insertQb
635
+ .insertFrom(selectQb, { columns: insertColumns })
636
+ .execute('run', false));
637
+ if (tableMeta === rootMeta) {
638
+ rootResult = res;
639
+ newPk = res.insertId ?? res.row?.[rootMeta.primaryKeys[0]];
640
+ }
641
+ }
642
+ return rootResult;
643
+ }
644
+ mapCloneOverrides(overrides, meta, options) {
645
+ if (!overrides) {
646
+ return undefined;
647
+ }
648
+ return super.mapDataToFieldNames(overrides, true, meta.properties, options.convertCustomTypes);
649
+ }
650
+ buildCloneFields(props, mappedOverrides, meta) {
651
+ const selectFields = [];
652
+ const insertColumns = [];
653
+ for (const prop of props) {
654
+ for (const fieldName of prop.fieldNames) {
655
+ insertColumns.push(fieldName);
656
+ if (mappedOverrides && fieldName in mappedOverrides) {
657
+ selectFields.push(raw('? as ??', [mappedOverrides[fieldName], fieldName]));
658
+ }
659
+ else if (meta.versionProperty === prop.name) {
660
+ const initial = prop.runtimeType === 'Date' ? new Date() : 1;
661
+ selectFields.push(raw('? as ??', [initial, fieldName]));
662
+ }
663
+ else {
664
+ selectFields.push(fieldName);
665
+ }
666
+ }
667
+ }
668
+ return { selectFields, insertColumns };
669
+ }
670
+ getCloneableProps(meta, ownProps) {
671
+ return (ownProps ? (meta.ownProps ?? meta.props) : meta.props).filter(prop => {
672
+ if (prop.persist === false) {
673
+ return false;
674
+ }
675
+ if (prop.primary) {
676
+ return false;
677
+ }
678
+ if (!prop.fieldNames?.length) {
679
+ return false;
680
+ }
681
+ if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
682
+ return false;
683
+ }
684
+ if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
685
+ return false;
686
+ }
687
+ return true;
688
+ });
689
+ }
577
690
  async nativeInsertMany(entityName, data, options = {}, transform) {
578
691
  options.processCollections ??= true;
579
692
  options.convertCustomTypes ??= true;
@@ -996,8 +1109,29 @@ export class AbstractSqlDriver extends DatabaseDriver {
996
1109
  const pks = wrapped.getPrimaryKeys(true);
997
1110
  const snap = coll.getSnapshot();
998
1111
  const includes = (arr, item) => !!arr.find(i => this.comparePrimaryKeyArrays(i, item));
999
- const snapshot = snap ? snap.map(item => helper(item).getPrimaryKeys(true)) : [];
1000
- const current = coll.getItems(false).map(item => helper(item).getPrimaryKeys(true));
1112
+ // For union-target polymorphic M:N, prepend the per-row discriminator value so the pivot
1113
+ // persister can write it alongside the FK id. Memoized per sync-run because a collection can
1114
+ // hold hundreds of items of the same few types, and findDiscriminatorValue walks the prototype
1115
+ // chain + re-scans Object.entries each call.
1116
+ const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(coll.property);
1117
+ const classToDisc = new Map();
1118
+ const toDiff = (item) => {
1119
+ const keys = helper(item).getPrimaryKeys(true);
1120
+ if (!isUnionTargetMN) {
1121
+ return keys;
1122
+ }
1123
+ let disc = classToDisc.get(item.constructor);
1124
+ if (!classToDisc.has(item.constructor)) {
1125
+ disc = QueryHelper.findDiscriminatorValue(coll.property.discriminatorMap, item.constructor);
1126
+ if (disc === undefined) {
1127
+ 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.`);
1128
+ }
1129
+ classToDisc.set(item.constructor, disc);
1130
+ }
1131
+ return [disc, ...keys];
1132
+ };
1133
+ const snapshot = snap ? snap.map(toDiff) : [];
1134
+ const current = coll.getItems(false).map(toDiff);
1001
1135
  const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
1002
1136
  const insertDiff = current.filter(item => !includes(snapshot, item));
1003
1137
  const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
@@ -1069,21 +1203,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
1069
1203
  return {};
1070
1204
  }
1071
1205
  const pivotMeta = this.metadata.get(prop.pivotEntity);
1206
+ if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
1207
+ return this.loadFromUnionTargetPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
1208
+ }
1072
1209
  if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
1073
1210
  return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
1074
1211
  }
1075
1212
  const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
1076
1213
  const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
1077
1214
  const ownerMeta = pivotProp2.targetMeta;
1078
- // The pivot query builder doesn't convert custom types, so we need to manually
1079
- // convert owner PKs to DB format for the query and convert result FKs back to
1080
- // JS format for consistent key hashing in buildPivotResultMap.
1081
- const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
1082
- const needsConversion = pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
1083
- let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
1084
- if (needsConversion) {
1085
- ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
1086
- }
1215
+ const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
1087
1216
  const cond = {
1088
1217
  [pivotProp2.name]: { $in: ownerPks },
1089
1218
  };
@@ -1098,7 +1227,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1098
1227
  const fields = pivotJoin
1099
1228
  ? [pivotProp1.name, pivotProp2.name]
1100
1229
  : [pivotProp1.name, pivotProp2.name, ...childFields];
1101
- const res = await this.find(pivotMeta.class, where, {
1230
+ const pivotFindOptions = {
1102
1231
  ctx,
1103
1232
  ...options,
1104
1233
  fields,
@@ -1114,10 +1243,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
1114
1243
  },
1115
1244
  ],
1116
1245
  populateWhere: undefined,
1117
- // @ts-ignore
1118
1246
  _populateWhere: 'infer',
1119
1247
  populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
1120
- });
1248
+ };
1249
+ if (pivotFindOptions._partitionLimit) {
1250
+ pivotFindOptions._partitionLimit.partitionBy = pivotProp2.name;
1251
+ }
1252
+ const res = await this.find(pivotMeta.class, where, pivotFindOptions);
1121
1253
  // Convert result FK values back to JS format so key hashing
1122
1254
  // in buildPivotResultMap is consistent with the owner keys.
1123
1255
  if (needsConversion) {
@@ -1237,6 +1369,102 @@ export class AbstractSqlDriver extends DatabaseDriver {
1237
1369
  });
1238
1370
  return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
1239
1371
  }
1372
+ /**
1373
+ * Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
1374
+ * Each pivot row's discriminator column selects which target table to hydrate.
1375
+ */
1376
+ async loadFromUnionTargetPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options = {}, pivotJoin) {
1377
+ // :ref hints cannot be honored for union-target — EntityLoader.getReference needs a concrete
1378
+ // class per item, but the ref-mode map only carries flat PK values and would hydrate every
1379
+ // row as the first polymorph target. Fail loudly instead of silently corrupting the collection.
1380
+ if (pivotJoin) {
1381
+ 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.`);
1382
+ }
1383
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
1384
+ const targets = prop.polymorphTargets;
1385
+ const ownerProp = pivotMeta.relations.find(r => r.persist !== false && !r.polymorphic);
1386
+ const discriminatorColumn = prop.discriminatorColumn;
1387
+ const ownerMeta = ownerProp.targetMeta;
1388
+ const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
1389
+ const pivotRows = (await this.find(pivotMeta.class, { [ownerProp.name]: { $in: ownerPks } }, {
1390
+ ctx,
1391
+ orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
1392
+ fields: [ownerProp.name, discriminatorColumn, prop.discriminator],
1393
+ populateWhere: undefined,
1394
+ // @ts-ignore
1395
+ _populateWhere: 'infer',
1396
+ }));
1397
+ /* v8 ignore next 7 - custom-type PK conversion, tested via loadFromPivotTable path */
1398
+ if (needsConversion) {
1399
+ for (const item of pivotRows) {
1400
+ const fk = item[ownerProp.name];
1401
+ if (fk != null) {
1402
+ item[ownerProp.name] = pkProp.customType.convertToJSValue(fk, this.platform);
1403
+ }
1404
+ }
1405
+ }
1406
+ const classMeta = new Map(targets.map(t => [t.class, t]));
1407
+ const rowsByTarget = new Map();
1408
+ for (const row of pivotRows) {
1409
+ const discValue = row[discriminatorColumn];
1410
+ const targetClass = prop.discriminatorMap[discValue];
1411
+ const targetMeta = classMeta.get(targetClass);
1412
+ /* v8 ignore next 3 - defensive: unknown discriminator value */
1413
+ if (!targetMeta) {
1414
+ continue;
1415
+ }
1416
+ const list = rowsByTarget.get(targetMeta) ?? [];
1417
+ list.push(row);
1418
+ rowsByTarget.set(targetMeta, list);
1419
+ }
1420
+ // Strip the outer find's orderBy/fields/exclude before bulk-loading targets by PK — those apply
1421
+ // to the owner query, not each polymorph target (Image and Video wouldn't share an orderBy field).
1422
+ // populateFilter is a filter on the populated collection; since union-target splits the pivot
1423
+ // and target queries, we merge it into the target-level `where` instead of wrapping it on the
1424
+ // pivot query (where joins to target tables aren't available).
1425
+ // Hoisted above the loop since `options` doesn't change per target.
1426
+ const { orderBy: _o, fields: _f, exclude: _e, populateFilter, ...childOptions } = options;
1427
+ const populate = options.populate ?? [];
1428
+ const orphanedRows = new Set();
1429
+ for (const [targetMeta, rows] of rowsByTarget) {
1430
+ const targetIds = rows.map(r => r[prop.discriminator]);
1431
+ // Union-target pivot stores one scalar FK per row; composite-PK targets are rejected at
1432
+ // metadata validation time, so a single primary key column is guaranteed here.
1433
+ const pkCol = targetMeta.primaryKeys[0];
1434
+ let cond = { [pkCol]: { $in: targetIds } };
1435
+ if (!Utils.isEmpty(where)) {
1436
+ cond = { $and: [cond, where] };
1437
+ }
1438
+ if (!Utils.isEmpty(populateFilter)) {
1439
+ cond = { $and: [cond, populateFilter] };
1440
+ }
1441
+ const results = (await this.find(targetMeta.class, cond, {
1442
+ ctx,
1443
+ ...childOptions,
1444
+ populate: populate,
1445
+ }));
1446
+ const byPk = new Map();
1447
+ for (const row of results) {
1448
+ Object.defineProperty(row, 'constructor', {
1449
+ value: targetMeta.class,
1450
+ enumerable: false,
1451
+ configurable: true,
1452
+ });
1453
+ byPk.set(Utils.getPrimaryKeyHash([row[pkCol]]), row);
1454
+ }
1455
+ for (const row of rows) {
1456
+ const pkHash = Utils.getPrimaryKeyHash([row[prop.discriminator]]);
1457
+ const entity = byPk.get(pkHash);
1458
+ if (entity == null) {
1459
+ orphanedRows.add(row);
1460
+ continue;
1461
+ }
1462
+ row[prop.discriminator] = entity;
1463
+ }
1464
+ }
1465
+ const result = orphanedRows.size > 0 ? pivotRows.filter(r => !orphanedRows.has(r)) : pivotRows;
1466
+ return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator);
1467
+ }
1240
1468
  /**
1241
1469
  * Build a map from owner PKs to their related entities from pivot table results.
1242
1470
  */
@@ -1261,6 +1489,21 @@ export class AbstractSqlDriver extends DatabaseDriver {
1261
1489
  }
1262
1490
  return undefined;
1263
1491
  }
1492
+ /**
1493
+ * The pivot query builder doesn't convert custom types — manually convert owner PKs to the DB
1494
+ * representation. Returns `needsConversion` + `pkProp` so the caller can convert result FKs back
1495
+ * to JS format for consistent key hashing in `buildPivotResultMap`.
1496
+ */
1497
+ convertOwnerPksForPivotQuery(owners, ownerMeta) {
1498
+ const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
1499
+ const needsConversion = !!pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
1500
+ let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
1501
+ /* v8 ignore next 4 - custom-type PK conversion, tested via loadFromPivotTable path */
1502
+ if (needsConversion) {
1503
+ ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
1504
+ }
1505
+ return { ownerPks, needsConversion, pkProp };
1506
+ }
1264
1507
  getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
1265
1508
  if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
1266
1509
  return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
@@ -1327,6 +1570,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
1327
1570
  if (ref && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
1328
1571
  return true;
1329
1572
  }
1573
+ // Union-target polymorphic M:N cannot be loaded via a single JOIN because rows span multiple
1574
+ // target tables; fall through to SELECT_IN which dispatches through `loadFromPivotTable`.
1575
+ // Polymorphic M:1 (to-one with target_type discriminator) is handled via LEFT JOINs elsewhere.
1576
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && QueryHelper.isUnionTargetPolymorphic(prop) && prop.owner) {
1577
+ return false;
1578
+ }
1330
1579
  // skip redundant joins for 1:1 owner population hints when using `mapToPk`
1331
1580
  if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
1332
1581
  return false;
@@ -1695,6 +1944,57 @@ export class AbstractSqlDriver extends DatabaseDriver {
1695
1944
  }
1696
1945
  return qb;
1697
1946
  }
1947
+ /**
1948
+ * Renders a `FilterQuery` predicate into a SQL fragment (without the `WHERE` keyword and
1949
+ * without table-alias prefixes) suitable for inlining into a partial-index DDL statement.
1950
+ * Used by `DatabaseTable.addIndex` when the user passes an object `where` on `@Index` /
1951
+ * `@Unique`. Strings are returned unchanged.
1952
+ */
1953
+ renderPartialIndexWhere(entityName, where) {
1954
+ if (typeof where === 'string') {
1955
+ return where;
1956
+ }
1957
+ const name = Utils.className(entityName);
1958
+ if (where == null || (Utils.isPlainObject(where) && Object.keys(where).length === 0)) {
1959
+ throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` is empty.`);
1960
+ }
1961
+ const alias = '__p';
1962
+ const qb = this.createQueryBuilder(entityName, undefined, undefined, undefined, undefined, alias);
1963
+ qb.where(where);
1964
+ const sql = qb.getFormattedQuery();
1965
+ // Relation traversal produces join clauses whose aliased identifiers can't be inlined
1966
+ // into a CREATE INDEX ... WHERE clause — reject with a clear error rather than emitting broken DDL.
1967
+ if (/\bjoin\b/i.test(sql.split(/\bwhere\b/i)[0])) {
1968
+ throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` may not traverse relations.`);
1969
+ }
1970
+ // Anchor at end-of-string only — the synthetic QB has no top-level order by / limit /
1971
+ // group by / having / offset, so any such keyword inside the captured predicate is
1972
+ // inside a subquery and must not terminate the match.
1973
+ const match = /\bwhere\s+([\s\S]+)$/i.exec(sql);
1974
+ if (!match) {
1975
+ throw new Error(`Failed to render partial-index predicate for entity '${name}': ${sql}`);
1976
+ }
1977
+ const quote = (s) => this.platform.quoteIdentifier(s);
1978
+ const aliasPrefix = new RegExp(`${quote(alias).replace(/[[\]]/g, '\\$&')}\\.`, 'g');
1979
+ const stripped = match[1].replace(aliasPrefix, '').trim();
1980
+ // Any qualified column reference remaining after the alias strip points at another table or
1981
+ // subquery and can't be inlined into a CREATE INDEX ... WHERE predicate. Covers both
1982
+ // QB-generated sub-aliases (quoted, e.g. `"e0"."col"`) and raw fragments with bare refs
1983
+ // (e.g. `raw('other_table.col = 1')`). String literals are erased first so dots inside
1984
+ // them (e.g. JSON path operands like `'$.path'`) don't trip the guard.
1985
+ // Both patterns use a `(?!\s*\()` lookahead so schema-qualified function calls
1986
+ // (`pg_catalog.lower(name)`, `"public".my_func(col)`) are accepted — only `<id>.<id>` not
1987
+ // followed by `(` is treated as a cross-table column reference.
1988
+ const withoutStrings = stripped.replace(/'(?:[^']|'')*'/g, "''");
1989
+ const quotedIdent = String.raw `(?:"(?:[^"]|"")+"|\`(?:[^\`]|\`\`)+\`|\[(?:[^\]]|\]\])+\])`;
1990
+ const anyIdent = `(?:${quotedIdent}|[A-Za-z_]\\w*)`;
1991
+ const quotedCrossRef = new RegExp(`${quotedIdent}\\s*\\.\\s*${anyIdent}(?!\\s*\\()`);
1992
+ const bareCrossRef = /\b[A-Za-z_]\w*\s*\.\s*[A-Za-z_]\w*\b(?!\s*\()/;
1993
+ if (quotedCrossRef.test(withoutStrings) || bareCrossRef.test(withoutStrings)) {
1994
+ throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` references another table or subquery which cannot be inlined into a CREATE INDEX ... WHERE clause.`);
1995
+ }
1996
+ return stripped;
1997
+ }
1698
1998
  resolveConnectionType(args) {
1699
1999
  if (args.ctx) {
1700
2000
  return 'write';
@@ -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 };
@@ -1,4 +1,4 @@
1
- import { type EntitySchemaWithMeta, EntityManager, type AnyEntity, type ConnectionType, type EntityData, type EntityName, type EntityRepository, type GetRepository, type QueryResult, type FilterQuery, type LoggingOptions, type RawQueryFragment } from '@mikro-orm/core';
1
+ import { type EntitySchemaWithMeta, EntityManager, type AnyEntity, type ConnectionType, type CountByOptions, type Dictionary, type EntityData, type EntityKey, type EntityName, type EntityRepository, type FilterQuery, type GetRepository, type LoggingOptions, type QueryResult, type RawQueryFragment } from '@mikro-orm/core';
2
2
  import type { AbstractSqlDriver } from './AbstractSqlDriver.js';
3
3
  import type { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
4
4
  import type { QueryBuilder } from './query/QueryBuilder.js';
@@ -29,6 +29,10 @@ export declare class SqlEntityManager<Driver extends AbstractSqlDriver = Abstrac
29
29
  getKysely<TDB = undefined, TOptions extends GetKyselyOptions = GetKyselyOptions>(options?: TOptions): Kysely<TDB extends undefined ? InferKyselyDB<EntitiesFromManager<this>, TOptions> & InferClassEntityDB<AllEntitiesFromManager<this>, TOptions> : TDB>;
30
30
  /** Executes a raw SQL query, using the current transaction context if available. */
31
31
  execute<T extends QueryResult | EntityData<AnyEntity> | EntityData<AnyEntity>[] = EntityData<AnyEntity>[]>(query: string | NativeQueryBuilder | RawQueryFragment, params?: any[], method?: 'all' | 'get' | 'run', loggerContext?: LoggingOptions): Promise<T>;
32
+ /**
33
+ * @inheritDoc
34
+ */
35
+ countBy<Entity extends object>(entityName: EntityName<Entity>, groupBy: EntityKey<Entity> | readonly EntityKey<Entity>[], options?: CountByOptions<Entity>): Promise<Dictionary<number>>;
32
36
  getRepository<T extends object, U extends EntityRepository<T> = SqlEntityRepository<T>>(entityName: EntityName<T>): GetRepository<T, U>;
33
37
  protected applyDiscriminatorCondition<Entity extends object>(entityName: EntityName<Entity>, where: FilterQuery<Entity>): FilterQuery<Entity>;
34
38
  }
@@ -1,4 +1,4 @@
1
- import { EntityManager, } from '@mikro-orm/core';
1
+ import { EntityManager, raw, Utils, } from '@mikro-orm/core';
2
2
  import { MikroKyselyPlugin } from './plugin/index.js';
3
3
  /**
4
4
  * @inheritDoc
@@ -35,6 +35,41 @@ export class SqlEntityManager extends EntityManager {
35
35
  async execute(query, params = [], method = 'all', loggerContext) {
36
36
  return this.getDriver().execute(query, params, method, this.getContext(false).getTransactionContext(), loggerContext);
37
37
  }
38
+ /**
39
+ * @inheritDoc
40
+ */
41
+ async countBy(entityName, groupBy, options = {}) {
42
+ const em = this.getContext(false);
43
+ options = { ...options };
44
+ em.prepareOptions(options);
45
+ const meta = em.getMetadata().find(entityName);
46
+ const fields = Utils.asArray(groupBy);
47
+ const { where: rawWhere, ...countOptions } = options;
48
+ await em.tryFlush(entityName, options);
49
+ const where = await em.processWhere(entityName, rawWhere ?? {}, options, 'read');
50
+ const qb = em.createQueryBuilder(meta.class);
51
+ qb
52
+ .select([...fields, raw('count(*) as cnt')])
53
+ .where(where)
54
+ .groupBy(fields);
55
+ if (countOptions.having) {
56
+ qb.having(countOptions.having);
57
+ }
58
+ if (countOptions.schema) {
59
+ qb.withSchema(countOptions.schema);
60
+ }
61
+ const rows = await qb.execute('all', { mapResults: false });
62
+ const results = {};
63
+ for (const row of rows) {
64
+ const keyParts = fields.map(f => {
65
+ const col = meta.properties[f]?.fieldNames?.[0] ?? f;
66
+ return String(row[col]);
67
+ });
68
+ const key = keyParts.join(Utils.PK_SEPARATOR);
69
+ results[key] = +row.cnt;
70
+ }
71
+ return results;
72
+ }
38
73
  getRepository(entityName) {
39
74
  return super.getRepository(entityName);
40
75
  }
@@ -56,6 +56,10 @@ export class MsSqlNativeQueryBuilder extends NativeQueryBuilder {
56
56
  return this.combineParts();
57
57
  }
58
58
  compileInsert() {
59
+ if (this.options.insertSubQuery) {
60
+ super.compileInsert();
61
+ return;
62
+ }
59
63
  if (!this.options.data) {
60
64
  throw new Error('No data provided');
61
65
  }
@@ -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
  }