@mikro-orm/sql 7.0.17-dev.9 → 7.0.18-dev.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 (50) hide show
  1. package/AbstractSqlConnection.d.ts +1 -1
  2. package/AbstractSqlConnection.js +27 -6
  3. package/AbstractSqlDriver.d.ts +25 -1
  4. package/AbstractSqlDriver.js +356 -20
  5. package/AbstractSqlPlatform.d.ts +13 -2
  6. package/AbstractSqlPlatform.js +16 -3
  7. package/PivotCollectionPersister.d.ts +2 -2
  8. package/PivotCollectionPersister.js +19 -3
  9. package/README.md +2 -1
  10. package/SqlEntityManager.d.ts +46 -3
  11. package/SqlEntityManager.js +77 -7
  12. package/SqlMikroORM.d.ts +4 -4
  13. package/dialects/mssql/MsSqlNativeQueryBuilder.js +4 -0
  14. package/dialects/mysql/BaseMySqlPlatform.d.ts +1 -0
  15. package/dialects/mysql/BaseMySqlPlatform.js +3 -0
  16. package/dialects/mysql/MySqlNativeQueryBuilder.js +11 -0
  17. package/dialects/mysql/MySqlSchemaHelper.d.ts +19 -3
  18. package/dialects/mysql/MySqlSchemaHelper.js +254 -21
  19. package/dialects/oracledb/OracleDialect.d.ts +1 -1
  20. package/dialects/oracledb/OracleDialect.js +2 -1
  21. package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
  22. package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
  23. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +8 -0
  24. package/dialects/postgresql/BasePostgreSqlPlatform.js +50 -0
  25. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +38 -1
  26. package/dialects/postgresql/PostgreSqlSchemaHelper.js +341 -6
  27. package/dialects/postgresql/index.d.ts +2 -0
  28. package/dialects/postgresql/index.js +2 -0
  29. package/dialects/postgresql/typeOverrides.d.ts +14 -0
  30. package/dialects/postgresql/typeOverrides.js +12 -0
  31. package/dialects/sqlite/SqliteSchemaHelper.d.ts +7 -1
  32. package/dialects/sqlite/SqliteSchemaHelper.js +131 -2
  33. package/package.json +4 -4
  34. package/query/NativeQueryBuilder.d.ts +6 -0
  35. package/query/NativeQueryBuilder.js +16 -1
  36. package/query/QueryBuilder.d.ts +83 -1
  37. package/query/QueryBuilder.js +181 -8
  38. package/schema/DatabaseSchema.d.ts +29 -2
  39. package/schema/DatabaseSchema.js +137 -0
  40. package/schema/DatabaseTable.d.ts +20 -1
  41. package/schema/DatabaseTable.js +62 -3
  42. package/schema/SchemaComparator.d.ts +19 -0
  43. package/schema/SchemaComparator.js +250 -1
  44. package/schema/SchemaHelper.d.ts +77 -1
  45. package/schema/SchemaHelper.js +279 -5
  46. package/schema/SqlSchemaGenerator.d.ts +2 -2
  47. package/schema/SqlSchemaGenerator.js +47 -10
  48. package/schema/partitioning.d.ts +13 -0
  49. package/schema/partitioning.js +326 -0
  50. package/typings.d.ts +69 -2
@@ -3,6 +3,28 @@ import { QueryBuilder } from './query/QueryBuilder.js';
3
3
  import { JoinType, QueryType } from './query/enums.js';
4
4
  import { SqlEntityManager } from './SqlEntityManager.js';
5
5
  import { PivotCollectionPersister } from './PivotCollectionPersister.js';
6
+ /** Extracts cancellation controls from any options bag that extends `AbortQueryOptions`. */
7
+ function pickAbortOptions(options) {
8
+ if (!options || (options.signal == null && options.inflightQueryAbortStrategy == null)) {
9
+ return undefined;
10
+ }
11
+ return {
12
+ signal: options.signal,
13
+ inflightQueryAbortStrategy: options.inflightQueryAbortStrategy,
14
+ };
15
+ }
16
+ /**
17
+ * Returns a `loggerContext` payload that carries the abort fields alongside any existing
18
+ * context. The connection layer strips them before logging — this avoids widening the public
19
+ * `Connection.execute()` signature.
20
+ */
21
+ function withAbortContext(loggerContext, options) {
22
+ const abort = pickAbortOptions(options);
23
+ if (!abort) {
24
+ return loggerContext;
25
+ }
26
+ return { ...loggerContext, ...abort };
27
+ }
6
28
  /** Base class for SQL database drivers, implementing find/insert/update/delete using QueryBuilder. */
7
29
  export class AbstractSqlDriver extends DatabaseDriver {
8
30
  [EntityManagerType];
@@ -34,6 +56,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
34
56
  return { alias, name: meta.tableName, schema: effectiveSchema, qualifiedName, toString: () => alias };
35
57
  }
36
58
  validateSqlOptions(options) {
59
+ if (options.using && !options.indexHint) {
60
+ const names = Utils.asArray(options.using);
61
+ const hint = this.platform.formatIndexHint(names);
62
+ if (hint) {
63
+ options.indexHint = hint;
64
+ }
65
+ }
37
66
  if (options.collation != null && typeof options.collation !== 'string') {
38
67
  throw new Error('Collation option for SQL drivers must be a string (collation name). Use a CollationOptions object only with MongoDB.');
39
68
  }
@@ -53,6 +82,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
53
82
  const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em)
54
83
  .withSchema(schema)
55
84
  .cache(false);
85
+ qb.setAbortOptions(pickAbortOptions(options));
56
86
  const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
57
87
  const orderBy = this.buildOrderBy(qb, meta, populate, options);
58
88
  const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
@@ -90,6 +120,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
90
120
  if (options.em) {
91
121
  await qb.applyJoinedFilters(options.em, options.filters);
92
122
  }
123
+ if (options._partitionLimit) {
124
+ qb.setPartitionLimit(options._partitionLimit);
125
+ }
93
126
  return qb;
94
127
  }
95
128
  async find(entityName, where, options = {}) {
@@ -206,7 +239,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
206
239
  const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
207
240
  native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
208
241
  const query = native.compile();
209
- const res = await this.execute(query.sql, query.params, 'all', options.ctx);
242
+ const res = await this.execute(query.sql, query.params, 'all', options.ctx, withAbortContext(options.loggerContext, options));
210
243
  if (type === QueryType.COUNT) {
211
244
  return res[0].count;
212
245
  }
@@ -223,7 +256,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
223
256
  native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
224
257
  const query = native.compile();
225
258
  const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
226
- const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, options.loggerContext);
259
+ const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, withAbortContext(options.loggerContext, options), options.chunkSize);
227
260
  for await (const row of res) {
228
261
  yield this.mapResult(row, meta);
229
262
  }
@@ -531,6 +564,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
531
564
  const joinedProps = this.joinedProps(meta, populate, options);
532
565
  const schema = this.getSchemaName(meta, options);
533
566
  const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
567
+ qb.setAbortOptions(pickAbortOptions(options));
534
568
  const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
535
569
  if (meta && !Utils.isEmpty(populate)) {
536
570
  this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
@@ -556,6 +590,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
556
590
  const meta = this.metadata.get(entityName);
557
591
  const collections = this.extractManyToMany(meta, data);
558
592
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
593
+ qb.setAbortOptions(pickAbortOptions(options));
559
594
  const res = await this.rethrow(qb.insert(data).execute('run', false));
560
595
  res.row = res.row || {};
561
596
  let pk;
@@ -576,6 +611,115 @@ export class AbstractSqlDriver extends DatabaseDriver {
576
611
  await this.processManyToMany(meta, pk, collections, false, options);
577
612
  return res;
578
613
  }
614
+ async nativeClone(entityName, where, overrides, options = {}) {
615
+ options.convertCustomTypes ??= true;
616
+ const meta = this.metadata.get(entityName);
617
+ if (meta.inheritanceType === 'tpt' || meta.tptParent) {
618
+ return this.nativeCloneTPT(meta, where, overrides, options);
619
+ }
620
+ return this.nativeCloneSimple(meta, where, overrides, options);
621
+ }
622
+ async nativeCloneSimple(meta, where, overrides, options = {}) {
623
+ const props = this.getCloneableProps(meta);
624
+ const mappedOverrides = this.mapCloneOverrides(overrides, meta, options);
625
+ const { selectFields, insertColumns } = this.buildCloneFields(props, mappedOverrides, meta);
626
+ const abort = pickAbortOptions(options);
627
+ const selectQb = this.createQueryBuilder(meta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
628
+ selectQb.select(selectFields).where(where);
629
+ selectQb.setAbortOptions(abort);
630
+ const insertQb = this.createQueryBuilder(meta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
631
+ insertQb.setAbortOptions(abort);
632
+ return this.rethrow(insertQb
633
+ .insertFrom(selectQb, { columns: insertColumns })
634
+ .execute('run', false));
635
+ }
636
+ async nativeCloneTPT(leafMeta, where, overrides, options = {}) {
637
+ const hierarchy = [];
638
+ let current = leafMeta;
639
+ while (current) {
640
+ hierarchy.unshift(current);
641
+ current = current.tptParent;
642
+ }
643
+ const rootMeta = hierarchy[0];
644
+ let newPk;
645
+ let rootResult;
646
+ for (const tableMeta of hierarchy) {
647
+ const props = this.getCloneableProps(tableMeta, true);
648
+ const mappedOverrides = this.mapCloneOverrides(overrides, tableMeta, options);
649
+ const { selectFields, insertColumns } = this.buildCloneFields(props, mappedOverrides, tableMeta);
650
+ // For child tables, prepend the new PK value
651
+ if (tableMeta !== rootMeta && newPk != null) {
652
+ for (const pkName of tableMeta.primaryKeys) {
653
+ const prop = tableMeta.properties[pkName];
654
+ for (const fieldName of prop.fieldNames) {
655
+ insertColumns.unshift(fieldName);
656
+ selectFields.unshift(raw('? as ??', [newPk, fieldName]));
657
+ }
658
+ }
659
+ }
660
+ const sourceWhere = tableMeta === rootMeta ? where : (Utils.extractPK(where, tableMeta) ?? where);
661
+ const abort = pickAbortOptions(options);
662
+ const selectQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
663
+ selectQb.select(selectFields).where(sourceWhere);
664
+ selectQb.setAbortOptions(abort);
665
+ const insertQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
666
+ insertQb.setAbortOptions(abort);
667
+ const res = await this.rethrow(insertQb
668
+ .insertFrom(selectQb, { columns: insertColumns })
669
+ .execute('run', false));
670
+ if (tableMeta === rootMeta) {
671
+ rootResult = res;
672
+ newPk = res.insertId ?? res.row?.[rootMeta.primaryKeys[0]];
673
+ }
674
+ }
675
+ return rootResult;
676
+ }
677
+ mapCloneOverrides(overrides, meta, options) {
678
+ if (!overrides) {
679
+ return undefined;
680
+ }
681
+ return super.mapDataToFieldNames(overrides, true, meta.properties, options.convertCustomTypes);
682
+ }
683
+ buildCloneFields(props, mappedOverrides, meta) {
684
+ const selectFields = [];
685
+ const insertColumns = [];
686
+ for (const prop of props) {
687
+ for (const fieldName of prop.fieldNames) {
688
+ insertColumns.push(fieldName);
689
+ if (mappedOverrides && fieldName in mappedOverrides) {
690
+ selectFields.push(raw('? as ??', [mappedOverrides[fieldName], fieldName]));
691
+ }
692
+ else if (meta.versionProperty === prop.name) {
693
+ const initial = prop.runtimeType === 'Date' ? new Date() : 1;
694
+ selectFields.push(raw('? as ??', [initial, fieldName]));
695
+ }
696
+ else {
697
+ selectFields.push(fieldName);
698
+ }
699
+ }
700
+ }
701
+ return { selectFields, insertColumns };
702
+ }
703
+ getCloneableProps(meta, ownProps) {
704
+ return (ownProps ? (meta.ownProps ?? meta.props) : meta.props).filter(prop => {
705
+ if (prop.persist === false) {
706
+ return false;
707
+ }
708
+ if (prop.primary) {
709
+ return false;
710
+ }
711
+ if (!prop.fieldNames?.length) {
712
+ return false;
713
+ }
714
+ if ([ReferenceKind.ONE_TO_MANY, ReferenceKind.MANY_TO_MANY].includes(prop.kind)) {
715
+ return false;
716
+ }
717
+ if (prop.kind === ReferenceKind.EMBEDDED && !prop.object) {
718
+ return false;
719
+ }
720
+ return true;
721
+ });
722
+ }
579
723
  async nativeInsertMany(entityName, data, options = {}, transform) {
580
724
  options.processCollections ??= true;
581
725
  options.convertCustomTypes ??= true;
@@ -727,7 +871,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
727
871
  if (transform) {
728
872
  sql = transform(sql);
729
873
  }
730
- const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
874
+ const res = await this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options));
731
875
  let pk;
732
876
  /* v8 ignore next */
733
877
  if (pks.length > 1) {
@@ -760,6 +904,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
760
904
  }
761
905
  if (Utils.hasObjectKeys(data)) {
762
906
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
907
+ qb.setAbortOptions(pickAbortOptions(options));
763
908
  if (options.upsert) {
764
909
  /* v8 ignore next */
765
910
  const uniqueFields = options.onConflictFields ??
@@ -805,6 +950,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
805
950
  ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
806
951
  : meta.primaryKeys);
807
952
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
953
+ qb.setAbortOptions(pickAbortOptions(options));
808
954
  const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
809
955
  qb.insert(data)
810
956
  .onConflict(uniqueFields)
@@ -950,7 +1096,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
950
1096
  if (transform) {
951
1097
  sql = transform(sql, params);
952
1098
  }
953
- const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, options.loggerContext));
1099
+ const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options)));
954
1100
  for (let i = 0; i < collections.length; i++) {
955
1101
  await this.processManyToMany(meta, where[i], collections[i], false, options);
956
1102
  }
@@ -968,6 +1114,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
968
1114
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
969
1115
  .delete(where)
970
1116
  .withSchema(this.getSchemaName(meta, options));
1117
+ qb.setAbortOptions(pickAbortOptions(options));
971
1118
  return this.rethrow(qb.execute('run', false));
972
1119
  }
973
1120
  /**
@@ -998,8 +1145,29 @@ export class AbstractSqlDriver extends DatabaseDriver {
998
1145
  const pks = wrapped.getPrimaryKeys(true);
999
1146
  const snap = coll.getSnapshot();
1000
1147
  const includes = (arr, item) => !!arr.find(i => this.comparePrimaryKeyArrays(i, item));
1001
- const snapshot = snap ? snap.map(item => helper(item).getPrimaryKeys(true)) : [];
1002
- const current = coll.getItems(false).map(item => helper(item).getPrimaryKeys(true));
1148
+ // For union-target polymorphic M:N, prepend the per-row discriminator value so the pivot
1149
+ // persister can write it alongside the FK id. Memoized per sync-run because a collection can
1150
+ // hold hundreds of items of the same few types, and findDiscriminatorValue walks the prototype
1151
+ // chain + re-scans Object.entries each call.
1152
+ const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(coll.property);
1153
+ const classToDisc = new Map();
1154
+ const toDiff = (item) => {
1155
+ const keys = helper(item).getPrimaryKeys(true);
1156
+ if (!isUnionTargetMN) {
1157
+ return keys;
1158
+ }
1159
+ let disc = classToDisc.get(item.constructor);
1160
+ if (!classToDisc.has(item.constructor)) {
1161
+ disc = QueryHelper.findDiscriminatorValue(coll.property.discriminatorMap, item.constructor);
1162
+ if (disc === undefined) {
1163
+ 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.`);
1164
+ }
1165
+ classToDisc.set(item.constructor, disc);
1166
+ }
1167
+ return [disc, ...keys];
1168
+ };
1169
+ const snapshot = snap ? snap.map(toDiff) : [];
1170
+ const current = coll.getItems(false).map(toDiff);
1003
1171
  const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
1004
1172
  const insertDiff = current.filter(item => !includes(snapshot, item));
1005
1173
  const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
@@ -1017,6 +1185,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1017
1185
  if (coll.property.kind === ReferenceKind.ONE_TO_MANY) {
1018
1186
  const cols = coll.property.referencedColumnNames;
1019
1187
  const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(this.getSchemaName(meta, options));
1188
+ qb.setAbortOptions(pickAbortOptions(options));
1020
1189
  if (coll.getSnapshot() === undefined) {
1021
1190
  if (coll.property.orphanRemoval) {
1022
1191
  const query = qb
@@ -1058,7 +1227,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1058
1227
  schema = this.config.get('schema');
1059
1228
  }
1060
1229
  const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
1061
- const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext));
1230
+ const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext, pickAbortOptions(options)));
1062
1231
  persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks, coll.isInitialized());
1063
1232
  }
1064
1233
  for (const persister of Utils.values(groups)) {
@@ -1071,21 +1240,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
1071
1240
  return {};
1072
1241
  }
1073
1242
  const pivotMeta = this.metadata.get(prop.pivotEntity);
1243
+ if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
1244
+ return this.loadFromUnionTargetPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
1245
+ }
1074
1246
  if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
1075
1247
  return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
1076
1248
  }
1077
1249
  const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
1078
1250
  const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
1079
1251
  const ownerMeta = pivotProp2.targetMeta;
1080
- // The pivot query builder doesn't convert custom types, so we need to manually
1081
- // convert owner PKs to DB format for the query and convert result FKs back to
1082
- // JS format for consistent key hashing in buildPivotResultMap.
1083
- const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
1084
- const needsConversion = pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
1085
- let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
1086
- if (needsConversion) {
1087
- ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
1088
- }
1252
+ const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
1089
1253
  const cond = {
1090
1254
  [pivotProp2.name]: { $in: ownerPks },
1091
1255
  };
@@ -1100,7 +1264,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1100
1264
  const fields = pivotJoin
1101
1265
  ? [pivotProp1.name, pivotProp2.name]
1102
1266
  : [pivotProp1.name, pivotProp2.name, ...childFields];
1103
- const res = await this.find(pivotMeta.class, where, {
1267
+ const pivotFindOptions = {
1104
1268
  ctx,
1105
1269
  ...options,
1106
1270
  fields,
@@ -1116,10 +1280,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
1116
1280
  },
1117
1281
  ],
1118
1282
  populateWhere: undefined,
1119
- // @ts-ignore
1120
1283
  _populateWhere: 'infer',
1121
1284
  populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
1122
- });
1285
+ };
1286
+ if (pivotFindOptions._partitionLimit) {
1287
+ pivotFindOptions._partitionLimit.partitionBy = pivotProp2.name;
1288
+ }
1289
+ const res = await this.find(pivotMeta.class, where, pivotFindOptions);
1123
1290
  // Convert result FK values back to JS format so key hashing
1124
1291
  // in buildPivotResultMap is consistent with the owner keys.
1125
1292
  if (needsConversion) {
@@ -1239,6 +1406,102 @@ export class AbstractSqlDriver extends DatabaseDriver {
1239
1406
  });
1240
1407
  return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
1241
1408
  }
1409
+ /**
1410
+ * Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
1411
+ * Each pivot row's discriminator column selects which target table to hydrate.
1412
+ */
1413
+ async loadFromUnionTargetPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options = {}, pivotJoin) {
1414
+ // :ref hints cannot be honored for union-target — EntityLoader.getReference needs a concrete
1415
+ // class per item, but the ref-mode map only carries flat PK values and would hydrate every
1416
+ // row as the first polymorph target. Fail loudly instead of silently corrupting the collection.
1417
+ if (pivotJoin) {
1418
+ 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.`);
1419
+ }
1420
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
1421
+ const targets = prop.polymorphTargets;
1422
+ const ownerProp = pivotMeta.relations.find(r => r.persist !== false && !r.polymorphic);
1423
+ const discriminatorColumn = prop.discriminatorColumn;
1424
+ const ownerMeta = ownerProp.targetMeta;
1425
+ const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
1426
+ const pivotRows = (await this.find(pivotMeta.class, { [ownerProp.name]: { $in: ownerPks } }, {
1427
+ ctx,
1428
+ orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
1429
+ fields: [ownerProp.name, discriminatorColumn, prop.discriminator],
1430
+ populateWhere: undefined,
1431
+ // @ts-ignore
1432
+ _populateWhere: 'infer',
1433
+ }));
1434
+ /* v8 ignore next 7 - custom-type PK conversion, tested via loadFromPivotTable path */
1435
+ if (needsConversion) {
1436
+ for (const item of pivotRows) {
1437
+ const fk = item[ownerProp.name];
1438
+ if (fk != null) {
1439
+ item[ownerProp.name] = pkProp.customType.convertToJSValue(fk, this.platform);
1440
+ }
1441
+ }
1442
+ }
1443
+ const classMeta = new Map(targets.map(t => [t.class, t]));
1444
+ const rowsByTarget = new Map();
1445
+ for (const row of pivotRows) {
1446
+ const discValue = row[discriminatorColumn];
1447
+ const targetClass = prop.discriminatorMap[discValue];
1448
+ const targetMeta = classMeta.get(targetClass);
1449
+ /* v8 ignore next 3 - defensive: unknown discriminator value */
1450
+ if (!targetMeta) {
1451
+ continue;
1452
+ }
1453
+ const list = rowsByTarget.get(targetMeta) ?? [];
1454
+ list.push(row);
1455
+ rowsByTarget.set(targetMeta, list);
1456
+ }
1457
+ // Strip the outer find's orderBy/fields/exclude before bulk-loading targets by PK — those apply
1458
+ // to the owner query, not each polymorph target (Image and Video wouldn't share an orderBy field).
1459
+ // populateFilter is a filter on the populated collection; since union-target splits the pivot
1460
+ // and target queries, we merge it into the target-level `where` instead of wrapping it on the
1461
+ // pivot query (where joins to target tables aren't available).
1462
+ // Hoisted above the loop since `options` doesn't change per target.
1463
+ const { orderBy: _o, fields: _f, exclude: _e, populateFilter, ...childOptions } = options;
1464
+ const populate = options.populate ?? [];
1465
+ const orphanedRows = new Set();
1466
+ for (const [targetMeta, rows] of rowsByTarget) {
1467
+ const targetIds = rows.map(r => r[prop.discriminator]);
1468
+ // Union-target pivot stores one scalar FK per row; composite-PK targets are rejected at
1469
+ // metadata validation time, so a single primary key column is guaranteed here.
1470
+ const pkCol = targetMeta.primaryKeys[0];
1471
+ let cond = { [pkCol]: { $in: targetIds } };
1472
+ if (!Utils.isEmpty(where)) {
1473
+ cond = { $and: [cond, where] };
1474
+ }
1475
+ if (!Utils.isEmpty(populateFilter)) {
1476
+ cond = { $and: [cond, populateFilter] };
1477
+ }
1478
+ const results = (await this.find(targetMeta.class, cond, {
1479
+ ctx,
1480
+ ...childOptions,
1481
+ populate: populate,
1482
+ }));
1483
+ const byPk = new Map();
1484
+ for (const row of results) {
1485
+ Object.defineProperty(row, 'constructor', {
1486
+ value: targetMeta.class,
1487
+ enumerable: false,
1488
+ configurable: true,
1489
+ });
1490
+ byPk.set(Utils.getPrimaryKeyHash([row[pkCol]]), row);
1491
+ }
1492
+ for (const row of rows) {
1493
+ const pkHash = Utils.getPrimaryKeyHash([row[prop.discriminator]]);
1494
+ const entity = byPk.get(pkHash);
1495
+ if (entity == null) {
1496
+ orphanedRows.add(row);
1497
+ continue;
1498
+ }
1499
+ row[prop.discriminator] = entity;
1500
+ }
1501
+ }
1502
+ const result = orphanedRows.size > 0 ? pivotRows.filter(r => !orphanedRows.has(r)) : pivotRows;
1503
+ return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator);
1504
+ }
1242
1505
  /**
1243
1506
  * Build a map from owner PKs to their related entities from pivot table results.
1244
1507
  */
@@ -1263,6 +1526,21 @@ export class AbstractSqlDriver extends DatabaseDriver {
1263
1526
  }
1264
1527
  return undefined;
1265
1528
  }
1529
+ /**
1530
+ * The pivot query builder doesn't convert custom types — manually convert owner PKs to the DB
1531
+ * representation. Returns `needsConversion` + `pkProp` so the caller can convert result FKs back
1532
+ * to JS format for consistent key hashing in `buildPivotResultMap`.
1533
+ */
1534
+ convertOwnerPksForPivotQuery(owners, ownerMeta) {
1535
+ const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
1536
+ const needsConversion = !!pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
1537
+ let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
1538
+ /* v8 ignore next 4 - custom-type PK conversion, tested via loadFromPivotTable path */
1539
+ if (needsConversion) {
1540
+ ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
1541
+ }
1542
+ return { ownerPks, needsConversion, pkProp };
1543
+ }
1266
1544
  getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
1267
1545
  if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
1268
1546
  return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
@@ -1329,6 +1607,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
1329
1607
  if (ref && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
1330
1608
  return true;
1331
1609
  }
1610
+ // Union-target polymorphic M:N cannot be loaded via a single JOIN because rows span multiple
1611
+ // target tables; fall through to SELECT_IN which dispatches through `loadFromPivotTable`.
1612
+ // Polymorphic M:1 (to-one with target_type discriminator) is handled via LEFT JOINs elsewhere.
1613
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && QueryHelper.isUnionTargetPolymorphic(prop) && prop.owner) {
1614
+ return false;
1615
+ }
1332
1616
  // skip redundant joins for 1:1 owner population hints when using `mapToPk`
1333
1617
  if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
1334
1618
  return false;
@@ -1713,6 +1997,57 @@ export class AbstractSqlDriver extends DatabaseDriver {
1713
1997
  }
1714
1998
  return qb;
1715
1999
  }
2000
+ /**
2001
+ * Renders a `FilterQuery` predicate into a SQL fragment (without the `WHERE` keyword and
2002
+ * without table-alias prefixes) suitable for inlining into a partial-index DDL statement.
2003
+ * Used by `DatabaseTable.addIndex` when the user passes an object `where` on `@Index` /
2004
+ * `@Unique`. Strings are returned unchanged.
2005
+ */
2006
+ renderPartialIndexWhere(entityName, where) {
2007
+ if (typeof where === 'string') {
2008
+ return where;
2009
+ }
2010
+ const name = Utils.className(entityName);
2011
+ if (where == null || (Utils.isPlainObject(where) && Object.keys(where).length === 0)) {
2012
+ throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` is empty.`);
2013
+ }
2014
+ const alias = '__p';
2015
+ const qb = this.createQueryBuilder(entityName, undefined, undefined, undefined, undefined, alias);
2016
+ qb.where(where);
2017
+ const sql = qb.getFormattedQuery();
2018
+ // Relation traversal produces join clauses whose aliased identifiers can't be inlined
2019
+ // into a CREATE INDEX ... WHERE clause — reject with a clear error rather than emitting broken DDL.
2020
+ if (/\bjoin\b/i.test(sql.split(/\bwhere\b/i)[0])) {
2021
+ throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` may not traverse relations.`);
2022
+ }
2023
+ // Anchor at end-of-string only — the synthetic QB has no top-level order by / limit /
2024
+ // group by / having / offset, so any such keyword inside the captured predicate is
2025
+ // inside a subquery and must not terminate the match.
2026
+ const match = /\bwhere\s+([\s\S]+)$/i.exec(sql);
2027
+ if (!match) {
2028
+ throw new Error(`Failed to render partial-index predicate for entity '${name}': ${sql}`);
2029
+ }
2030
+ const quote = (s) => this.platform.quoteIdentifier(s);
2031
+ const aliasPrefix = new RegExp(`${quote(alias).replace(/[[\]]/g, '\\$&')}\\.`, 'g');
2032
+ const stripped = match[1].replace(aliasPrefix, '').trim();
2033
+ // Any qualified column reference remaining after the alias strip points at another table or
2034
+ // subquery and can't be inlined into a CREATE INDEX ... WHERE predicate. Covers both
2035
+ // QB-generated sub-aliases (quoted, e.g. `"e0"."col"`) and raw fragments with bare refs
2036
+ // (e.g. `raw('other_table.col = 1')`). String literals are erased first so dots inside
2037
+ // them (e.g. JSON path operands like `'$.path'`) don't trip the guard.
2038
+ // Both patterns use a `(?!\s*\()` lookahead so schema-qualified function calls
2039
+ // (`pg_catalog.lower(name)`, `"public".my_func(col)`) are accepted — only `<id>.<id>` not
2040
+ // followed by `(` is treated as a cross-table column reference.
2041
+ const withoutStrings = stripped.replace(/'(?:[^']|'')*'/g, "''");
2042
+ const quotedIdent = String.raw `(?:"(?:[^"]|"")+"|\`(?:[^\`]|\`\`)+\`|\[(?:[^\]]|\]\])+\])`;
2043
+ const anyIdent = `(?:${quotedIdent}|[A-Za-z_]\\w*)`;
2044
+ const quotedCrossRef = new RegExp(`${quotedIdent}\\s*\\.\\s*${anyIdent}(?!\\s*\\()`);
2045
+ const bareCrossRef = /\b[A-Za-z_]\w*\s*\.\s*[A-Za-z_]\w*\b(?!\s*\()/;
2046
+ if (quotedCrossRef.test(withoutStrings) || bareCrossRef.test(withoutStrings)) {
2047
+ 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.`);
2048
+ }
2049
+ return stripped;
2050
+ }
1716
2051
  resolveConnectionType(args) {
1717
2052
  if (args.ctx) {
1718
2053
  return 'write';
@@ -1739,7 +2074,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1739
2074
  for (const prop of meta.relations) {
1740
2075
  if (collections[prop.name]) {
1741
2076
  const pivotMeta = this.metadata.get(prop.pivotEntity);
1742
- const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
2077
+ const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext, pickAbortOptions(options));
1743
2078
  persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
1744
2079
  await this.rethrow(persister.execute());
1745
2080
  }
@@ -1748,6 +2083,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1748
2083
  async lockPessimistic(entity, options) {
1749
2084
  const meta = helper(entity).__meta;
1750
2085
  const qb = this.createQueryBuilder(meta.class, options.ctx, undefined, undefined, options.logging).withSchema(options.schema ?? meta.schema);
2086
+ qb.setAbortOptions(pickAbortOptions(options));
1751
2087
  const cond = Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
1752
2088
  qb.select(raw('1'))
1753
2089
  .where(cond)
@@ -51,8 +51,19 @@ export declare abstract class AbstractSqlPlatform extends Platform {
51
51
  * @internal
52
52
  */
53
53
  quoteCollation(collation: string): string;
54
- /** @internal */
55
- protected validateCollationName(collation: string): void;
54
+ /**
55
+ * PG ICU locale names include hyphens (`en-US-x-icu`) and libc locales include dots (`en_US.utf8`),
56
+ * so word-chars alone would reject valid real-world collations.
57
+ * @internal
58
+ */
59
+ validateCollationName(collation: string): void;
60
+ /**
61
+ * Whether collation names compare case-insensitively in this dialect. MySQL/MariaDB, MSSQL, and
62
+ * SQLite use case-insensitive collation identifiers; PostgreSQL stores them as case-sensitive
63
+ * names in `pg_collation` (e.g. `en-US-x-icu` is distinct from `EN-US-X-ICU`).
64
+ * @internal
65
+ */
66
+ caseInsensitiveCollationNames(): boolean;
56
67
  /** @internal */
57
68
  validateJsonPropertyName(name: string): void;
58
69
  /**
@@ -128,12 +128,25 @@ export class AbstractSqlPlatform extends Platform {
128
128
  this.validateCollationName(collation);
129
129
  return this.quoteIdentifier(collation);
130
130
  }
131
- /** @internal */
131
+ /**
132
+ * PG ICU locale names include hyphens (`en-US-x-icu`) and libc locales include dots (`en_US.utf8`),
133
+ * so word-chars alone would reject valid real-world collations.
134
+ * @internal
135
+ */
132
136
  validateCollationName(collation) {
133
- if (!/^[\w]+$/.test(collation)) {
134
- throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters.`);
137
+ if (!/^[\w\-.]+$/.test(collation)) {
138
+ throw new Error(`Invalid collation name: '${collation}'. Collation names must contain only word characters, hyphens, and dots.`);
135
139
  }
136
140
  }
141
+ /**
142
+ * Whether collation names compare case-insensitively in this dialect. MySQL/MariaDB, MSSQL, and
143
+ * SQLite use case-insensitive collation identifiers; PostgreSQL stores them as case-sensitive
144
+ * names in `pg_collation` (e.g. `en-US-x-icu` is distinct from `EN-US-X-ICU`).
145
+ * @internal
146
+ */
147
+ caseInsensitiveCollationNames() {
148
+ return true;
149
+ }
137
150
  /** @internal */
138
151
  validateJsonPropertyName(name) {
139
152
  if (!AbstractSqlPlatform.#JSON_PROPERTY_NAME_RE.test(name)) {
@@ -1,8 +1,8 @@
1
- import { type Dictionary, type EntityMetadata, type EntityProperty, type Primary, type Transaction } from '@mikro-orm/core';
1
+ import { type AbortQueryOptions, type Dictionary, type EntityMetadata, type EntityProperty, type Primary, type Transaction } from '@mikro-orm/core';
2
2
  import { type AbstractSqlDriver } from './AbstractSqlDriver.js';
3
3
  export declare class PivotCollectionPersister<Entity extends object> {
4
4
  #private;
5
- constructor(meta: EntityMetadata<Entity>, driver: AbstractSqlDriver, ctx?: Transaction, schema?: string, loggerContext?: Dictionary);
5
+ constructor(meta: EntityMetadata<Entity>, driver: AbstractSqlDriver, ctx?: Transaction, schema?: string, loggerContext?: Dictionary, abort?: AbortQueryOptions);
6
6
  enqueueUpdate(prop: EntityProperty<Entity>, insertDiff: Primary<Entity>[][], deleteDiff: Primary<Entity>[][] | boolean, pks: Primary<Entity>[], isInitialized?: boolean): void;
7
7
  private enqueueInsert;
8
8
  private enqueueUpsert;