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

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 (56) hide show
  1. package/AbstractSqlConnection.d.ts +1 -1
  2. package/AbstractSqlConnection.js +27 -6
  3. package/AbstractSqlDriver.d.ts +26 -1
  4. package/AbstractSqlDriver.js +294 -37
  5. package/AbstractSqlPlatform.d.ts +15 -3
  6. package/AbstractSqlPlatform.js +25 -7
  7. package/PivotCollectionPersister.d.ts +2 -2
  8. package/PivotCollectionPersister.js +19 -3
  9. package/README.md +2 -1
  10. package/SqlEntityManager.d.ts +48 -5
  11. package/SqlEntityManager.js +77 -7
  12. package/SqlMikroORM.d.ts +23 -0
  13. package/SqlMikroORM.js +23 -0
  14. package/dialects/mysql/BaseMySqlPlatform.d.ts +4 -5
  15. package/dialects/mysql/BaseMySqlPlatform.js +9 -10
  16. package/dialects/mysql/MySqlSchemaHelper.d.ts +19 -3
  17. package/dialects/mysql/MySqlSchemaHelper.js +280 -49
  18. package/dialects/oracledb/OracleDialect.d.ts +1 -1
  19. package/dialects/oracledb/OracleDialect.js +2 -1
  20. package/dialects/postgresql/BasePostgreSqlEntityManager.d.ts +19 -0
  21. package/dialects/postgresql/BasePostgreSqlEntityManager.js +24 -0
  22. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +11 -5
  23. package/dialects/postgresql/BasePostgreSqlPlatform.js +75 -17
  24. package/dialects/postgresql/PostgreSqlSchemaHelper.d.ts +38 -1
  25. package/dialects/postgresql/PostgreSqlSchemaHelper.js +362 -28
  26. package/dialects/postgresql/index.d.ts +2 -0
  27. package/dialects/postgresql/index.js +2 -0
  28. package/dialects/postgresql/typeOverrides.d.ts +14 -0
  29. package/dialects/postgresql/typeOverrides.js +12 -0
  30. package/dialects/sqlite/SqlitePlatform.d.ts +2 -1
  31. package/dialects/sqlite/SqlitePlatform.js +4 -0
  32. package/dialects/sqlite/SqliteSchemaHelper.d.ts +9 -2
  33. package/dialects/sqlite/SqliteSchemaHelper.js +148 -19
  34. package/index.d.ts +2 -0
  35. package/index.js +2 -0
  36. package/package.json +4 -4
  37. package/plugin/transformer.d.ts +11 -3
  38. package/plugin/transformer.js +138 -29
  39. package/query/CriteriaNode.d.ts +1 -1
  40. package/query/CriteriaNode.js +2 -2
  41. package/query/ObjectCriteriaNode.js +1 -1
  42. package/query/QueryBuilder.d.ts +42 -1
  43. package/query/QueryBuilder.js +78 -7
  44. package/schema/DatabaseSchema.d.ts +29 -2
  45. package/schema/DatabaseSchema.js +145 -4
  46. package/schema/DatabaseTable.d.ts +20 -1
  47. package/schema/DatabaseTable.js +182 -31
  48. package/schema/SchemaComparator.d.ts +19 -0
  49. package/schema/SchemaComparator.js +250 -1
  50. package/schema/SchemaHelper.d.ts +77 -1
  51. package/schema/SchemaHelper.js +297 -25
  52. package/schema/SqlSchemaGenerator.d.ts +2 -2
  53. package/schema/SqlSchemaGenerator.js +47 -10
  54. package/schema/partitioning.d.ts +13 -0
  55. package/schema/partitioning.js +326 -0
  56. package/typings.d.ts +72 -5
@@ -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
  }
@@ -50,7 +79,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
50
79
  const populate = this.autoJoinOneToOneOwner(meta, options.populate, options.fields);
51
80
  const joinedProps = this.joinedProps(meta, populate, options);
52
81
  const schema = this.getSchemaName(meta, options);
53
- const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em).withSchema(schema);
82
+ const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging, undefined, options.em)
83
+ .withSchema(schema)
84
+ .cache(false);
85
+ qb.setAbortOptions(pickAbortOptions(options));
54
86
  const fields = this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
55
87
  const orderBy = this.buildOrderBy(qb, meta, populate, options);
56
88
  const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
@@ -88,6 +120,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
88
120
  if (options.em) {
89
121
  await qb.applyJoinedFilters(options.em, options.filters);
90
122
  }
123
+ if (options._partitionLimit) {
124
+ qb.setPartitionLimit(options._partitionLimit);
125
+ }
91
126
  return qb;
92
127
  }
93
128
  async find(entityName, where, options = {}) {
@@ -204,7 +239,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
204
239
  const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
205
240
  native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
206
241
  const query = native.compile();
207
- 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));
208
243
  if (type === QueryType.COUNT) {
209
244
  return res[0].count;
210
245
  }
@@ -221,7 +256,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
221
256
  native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
222
257
  const query = native.compile();
223
258
  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);
259
+ const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, withAbortContext(options.loggerContext, options), options.chunkSize);
225
260
  for await (const row of res) {
226
261
  yield this.mapResult(row, meta);
227
262
  }
@@ -529,6 +564,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
529
564
  const joinedProps = this.joinedProps(meta, populate, options);
530
565
  const schema = this.getSchemaName(meta, options);
531
566
  const qb = this.createQueryBuilder(entityName, options.ctx, options.connectionType, false, options.logging);
567
+ qb.setAbortOptions(pickAbortOptions(options));
532
568
  const populateWhere = this.buildPopulateWhere(meta, joinedProps, options);
533
569
  if (meta && !Utils.isEmpty(populate)) {
534
570
  this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
@@ -554,6 +590,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
554
590
  const meta = this.metadata.get(entityName);
555
591
  const collections = this.extractManyToMany(meta, data);
556
592
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
593
+ qb.setAbortOptions(pickAbortOptions(options));
557
594
  const res = await this.rethrow(qb.insert(data).execute('run', false));
558
595
  res.row = res.row || {};
559
596
  let pk;
@@ -586,9 +623,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
586
623
  const props = this.getCloneableProps(meta);
587
624
  const mappedOverrides = this.mapCloneOverrides(overrides, meta, options);
588
625
  const { selectFields, insertColumns } = this.buildCloneFields(props, mappedOverrides, meta);
626
+ const abort = pickAbortOptions(options);
589
627
  const selectQb = this.createQueryBuilder(meta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
590
628
  selectQb.select(selectFields).where(where);
629
+ selectQb.setAbortOptions(abort);
591
630
  const insertQb = this.createQueryBuilder(meta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
631
+ insertQb.setAbortOptions(abort);
592
632
  return this.rethrow(insertQb
593
633
  .insertFrom(selectQb, { columns: insertColumns })
594
634
  .execute('run', false));
@@ -618,9 +658,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
618
658
  }
619
659
  }
620
660
  const sourceWhere = tableMeta === rootMeta ? where : (Utils.extractPK(where, tableMeta) ?? where);
661
+ const abort = pickAbortOptions(options);
621
662
  const selectQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'read', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
622
663
  selectQb.select(selectFields).where(sourceWhere);
664
+ selectQb.setAbortOptions(abort);
623
665
  const insertQb = this.createQueryBuilder(tableMeta.class, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(tableMeta, options));
666
+ insertQb.setAbortOptions(abort);
624
667
  const res = await this.rethrow(insertQb
625
668
  .insertFrom(selectQb, { columns: insertColumns })
626
669
  .execute('run', false));
@@ -795,13 +838,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
795
838
  else {
796
839
  const field = prop.fieldNames[0];
797
840
  if (!duplicates.includes(field) || !usedDups.includes(field)) {
841
+ const rowValue = row[prop.name];
842
+ const rowValueIsRaw = isRaw(rowValue);
798
843
  if (prop.customType &&
799
844
  !prop.object &&
800
845
  'convertToDatabaseValueSQL' in prop.customType &&
801
- row[prop.name] != null &&
802
- !isRaw(row[prop.name])) {
846
+ rowValue != null &&
847
+ !rowValueIsRaw) {
803
848
  keys.push(prop.customType.convertToDatabaseValueSQL('?', this.platform));
804
849
  }
850
+ else if (rowValueIsRaw && /^\s*(?:with|select)\b/i.test(rowValue.sql)) {
851
+ // raw subqueries must be parenthesized when inlined as a VALUES position
852
+ keys.push('(?)');
853
+ }
805
854
  else {
806
855
  keys.push('?');
807
856
  }
@@ -828,7 +877,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
828
877
  if (transform) {
829
878
  sql = transform(sql);
830
879
  }
831
- const res = await this.execute(sql, params, 'run', options.ctx, options.loggerContext);
880
+ const res = await this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options));
832
881
  let pk;
833
882
  /* v8 ignore next */
834
883
  if (pks.length > 1) {
@@ -861,6 +910,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
861
910
  }
862
911
  if (Utils.hasObjectKeys(data)) {
863
912
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
913
+ qb.setAbortOptions(pickAbortOptions(options));
864
914
  if (options.upsert) {
865
915
  /* v8 ignore next */
866
916
  const uniqueFields = options.onConflictFields ??
@@ -906,6 +956,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
906
956
  ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
907
957
  : meta.primaryKeys);
908
958
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
959
+ qb.setAbortOptions(pickAbortOptions(options));
909
960
  const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
910
961
  qb.insert(data)
911
962
  .onConflict(uniqueFields)
@@ -1051,7 +1102,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1051
1102
  if (transform) {
1052
1103
  sql = transform(sql, params);
1053
1104
  }
1054
- const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, options.loggerContext));
1105
+ const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, withAbortContext(options.loggerContext, options)));
1055
1106
  for (let i = 0; i < collections.length; i++) {
1056
1107
  await this.processManyToMany(meta, where[i], collections[i], false, options);
1057
1108
  }
@@ -1069,6 +1120,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1069
1120
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
1070
1121
  .delete(where)
1071
1122
  .withSchema(this.getSchemaName(meta, options));
1123
+ qb.setAbortOptions(pickAbortOptions(options));
1072
1124
  return this.rethrow(qb.execute('run', false));
1073
1125
  }
1074
1126
  /**
@@ -1099,8 +1151,29 @@ export class AbstractSqlDriver extends DatabaseDriver {
1099
1151
  const pks = wrapped.getPrimaryKeys(true);
1100
1152
  const snap = coll.getSnapshot();
1101
1153
  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));
1154
+ // For union-target polymorphic M:N, prepend the per-row discriminator value so the pivot
1155
+ // persister can write it alongside the FK id. Memoized per sync-run because a collection can
1156
+ // hold hundreds of items of the same few types, and findDiscriminatorValue walks the prototype
1157
+ // chain + re-scans Object.entries each call.
1158
+ const isUnionTargetMN = QueryHelper.isUnionTargetPolymorphic(coll.property);
1159
+ const classToDisc = new Map();
1160
+ const toDiff = (item) => {
1161
+ const keys = helper(item).getPrimaryKeys(true);
1162
+ if (!isUnionTargetMN) {
1163
+ return keys;
1164
+ }
1165
+ let disc = classToDisc.get(item.constructor);
1166
+ if (!classToDisc.has(item.constructor)) {
1167
+ disc = QueryHelper.findDiscriminatorValue(coll.property.discriminatorMap, item.constructor);
1168
+ if (disc === undefined) {
1169
+ 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.`);
1170
+ }
1171
+ classToDisc.set(item.constructor, disc);
1172
+ }
1173
+ return [disc, ...keys];
1174
+ };
1175
+ const snapshot = snap ? snap.map(toDiff) : [];
1176
+ const current = coll.getItems(false).map(toDiff);
1104
1177
  const deleteDiff = snap ? snapshot.filter(item => !includes(current, item)) : true;
1105
1178
  const insertDiff = current.filter(item => !includes(snapshot, item));
1106
1179
  const target = snapshot.filter(item => includes(current, item)).concat(...insertDiff);
@@ -1118,6 +1191,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1118
1191
  if (coll.property.kind === ReferenceKind.ONE_TO_MANY) {
1119
1192
  const cols = coll.property.referencedColumnNames;
1120
1193
  const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(this.getSchemaName(meta, options));
1194
+ qb.setAbortOptions(pickAbortOptions(options));
1121
1195
  if (coll.getSnapshot() === undefined) {
1122
1196
  if (coll.property.orphanRemoval) {
1123
1197
  const query = qb
@@ -1159,7 +1233,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1159
1233
  schema = this.config.get('schema');
1160
1234
  }
1161
1235
  const tableName = `${schema ?? '_'}.${pivotMeta.tableName}`;
1162
- const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext));
1236
+ const persister = (groups[tableName] ??= new PivotCollectionPersister(pivotMeta, this, options?.ctx, schema, options?.loggerContext, pickAbortOptions(options)));
1163
1237
  persister.enqueueUpdate(coll.property, insertDiff, deleteDiff, pks, coll.isInitialized());
1164
1238
  }
1165
1239
  for (const persister of Utils.values(groups)) {
@@ -1172,21 +1246,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
1172
1246
  return {};
1173
1247
  }
1174
1248
  const pivotMeta = this.metadata.get(prop.pivotEntity);
1249
+ if (prop.discriminatorColumn && QueryHelper.isUnionTargetPolymorphic(prop)) {
1250
+ return this.loadFromUnionTargetPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
1251
+ }
1175
1252
  if (prop.polymorphic && prop.discriminatorColumn && prop.discriminatorValue) {
1176
1253
  return this.loadFromPolymorphicPivotTable(prop, owners, where, orderBy, ctx, options, pivotJoin);
1177
1254
  }
1178
1255
  const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
1179
1256
  const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
1180
1257
  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
- }
1258
+ const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
1190
1259
  const cond = {
1191
1260
  [pivotProp2.name]: { $in: ownerPks },
1192
1261
  };
@@ -1201,7 +1270,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1201
1270
  const fields = pivotJoin
1202
1271
  ? [pivotProp1.name, pivotProp2.name]
1203
1272
  : [pivotProp1.name, pivotProp2.name, ...childFields];
1204
- const res = await this.find(pivotMeta.class, where, {
1273
+ const pivotFindOptions = {
1205
1274
  ctx,
1206
1275
  ...options,
1207
1276
  fields,
@@ -1217,10 +1286,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
1217
1286
  },
1218
1287
  ],
1219
1288
  populateWhere: undefined,
1220
- // @ts-ignore
1221
1289
  _populateWhere: 'infer',
1222
1290
  populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
1223
- });
1291
+ };
1292
+ if (pivotFindOptions._partitionLimit) {
1293
+ pivotFindOptions._partitionLimit.partitionBy = pivotProp2.name;
1294
+ }
1295
+ const res = await this.find(pivotMeta.class, where, pivotFindOptions);
1224
1296
  // Convert result FK values back to JS format so key hashing
1225
1297
  // in buildPivotResultMap is consistent with the owner keys.
1226
1298
  if (needsConversion) {
@@ -1340,6 +1412,102 @@ export class AbstractSqlDriver extends DatabaseDriver {
1340
1412
  });
1341
1413
  return this.buildPivotResultMap(owners, res, tagProp.name, ownerRelationName);
1342
1414
  }
1415
+ /**
1416
+ * Load a union-target polymorphic M:N pivot (e.g. Post.attachments -> Image | Video).
1417
+ * Each pivot row's discriminator column selects which target table to hydrate.
1418
+ */
1419
+ async loadFromUnionTargetPolymorphicPivotTable(prop, owners, where = {}, orderBy, ctx, options = {}, pivotJoin) {
1420
+ // :ref hints cannot be honored for union-target — EntityLoader.getReference needs a concrete
1421
+ // class per item, but the ref-mode map only carries flat PK values and would hydrate every
1422
+ // row as the first polymorph target. Fail loudly instead of silently corrupting the collection.
1423
+ if (pivotJoin) {
1424
+ 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.`);
1425
+ }
1426
+ const pivotMeta = this.metadata.get(prop.pivotEntity);
1427
+ const targets = prop.polymorphTargets;
1428
+ const ownerProp = pivotMeta.relations.find(r => r.persist !== false && !r.polymorphic);
1429
+ const discriminatorColumn = prop.discriminatorColumn;
1430
+ const ownerMeta = ownerProp.targetMeta;
1431
+ const { ownerPks, needsConversion, pkProp } = this.convertOwnerPksForPivotQuery(owners, ownerMeta);
1432
+ const pivotRows = (await this.find(pivotMeta.class, { [ownerProp.name]: { $in: ownerPks } }, {
1433
+ ctx,
1434
+ orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
1435
+ fields: [ownerProp.name, discriminatorColumn, prop.discriminator],
1436
+ populateWhere: undefined,
1437
+ // @ts-ignore
1438
+ _populateWhere: 'infer',
1439
+ }));
1440
+ /* v8 ignore next 7 - custom-type PK conversion, tested via loadFromPivotTable path */
1441
+ if (needsConversion) {
1442
+ for (const item of pivotRows) {
1443
+ const fk = item[ownerProp.name];
1444
+ if (fk != null) {
1445
+ item[ownerProp.name] = pkProp.customType.convertToJSValue(fk, this.platform);
1446
+ }
1447
+ }
1448
+ }
1449
+ const classMeta = new Map(targets.map(t => [t.class, t]));
1450
+ const rowsByTarget = new Map();
1451
+ for (const row of pivotRows) {
1452
+ const discValue = row[discriminatorColumn];
1453
+ const targetClass = prop.discriminatorMap[discValue];
1454
+ const targetMeta = classMeta.get(targetClass);
1455
+ /* v8 ignore next 3 - defensive: unknown discriminator value */
1456
+ if (!targetMeta) {
1457
+ continue;
1458
+ }
1459
+ const list = rowsByTarget.get(targetMeta) ?? [];
1460
+ list.push(row);
1461
+ rowsByTarget.set(targetMeta, list);
1462
+ }
1463
+ // Strip the outer find's orderBy/fields/exclude before bulk-loading targets by PK — those apply
1464
+ // to the owner query, not each polymorph target (Image and Video wouldn't share an orderBy field).
1465
+ // populateFilter is a filter on the populated collection; since union-target splits the pivot
1466
+ // and target queries, we merge it into the target-level `where` instead of wrapping it on the
1467
+ // pivot query (where joins to target tables aren't available).
1468
+ // Hoisted above the loop since `options` doesn't change per target.
1469
+ const { orderBy: _o, fields: _f, exclude: _e, populateFilter, ...childOptions } = options;
1470
+ const populate = options.populate ?? [];
1471
+ const orphanedRows = new Set();
1472
+ for (const [targetMeta, rows] of rowsByTarget) {
1473
+ const targetIds = rows.map(r => r[prop.discriminator]);
1474
+ // Union-target pivot stores one scalar FK per row; composite-PK targets are rejected at
1475
+ // metadata validation time, so a single primary key column is guaranteed here.
1476
+ const pkCol = targetMeta.primaryKeys[0];
1477
+ let cond = { [pkCol]: { $in: targetIds } };
1478
+ if (!Utils.isEmpty(where)) {
1479
+ cond = { $and: [cond, where] };
1480
+ }
1481
+ if (!Utils.isEmpty(populateFilter)) {
1482
+ cond = { $and: [cond, populateFilter] };
1483
+ }
1484
+ const results = (await this.find(targetMeta.class, cond, {
1485
+ ctx,
1486
+ ...childOptions,
1487
+ populate: populate,
1488
+ }));
1489
+ const byPk = new Map();
1490
+ for (const row of results) {
1491
+ Object.defineProperty(row, 'constructor', {
1492
+ value: targetMeta.class,
1493
+ enumerable: false,
1494
+ configurable: true,
1495
+ });
1496
+ byPk.set(Utils.getPrimaryKeyHash([row[pkCol]]), row);
1497
+ }
1498
+ for (const row of rows) {
1499
+ const pkHash = Utils.getPrimaryKeyHash([row[prop.discriminator]]);
1500
+ const entity = byPk.get(pkHash);
1501
+ if (entity == null) {
1502
+ orphanedRows.add(row);
1503
+ continue;
1504
+ }
1505
+ row[prop.discriminator] = entity;
1506
+ }
1507
+ }
1508
+ const result = orphanedRows.size > 0 ? pivotRows.filter(r => !orphanedRows.has(r)) : pivotRows;
1509
+ return this.buildPivotResultMap(owners, result, ownerProp.name, prop.discriminator);
1510
+ }
1343
1511
  /**
1344
1512
  * Build a map from owner PKs to their related entities from pivot table results.
1345
1513
  */
@@ -1364,6 +1532,21 @@ export class AbstractSqlDriver extends DatabaseDriver {
1364
1532
  }
1365
1533
  return undefined;
1366
1534
  }
1535
+ /**
1536
+ * The pivot query builder doesn't convert custom types — manually convert owner PKs to the DB
1537
+ * representation. Returns `needsConversion` + `pkProp` so the caller can convert result FKs back
1538
+ * to JS format for consistent key hashing in `buildPivotResultMap`.
1539
+ */
1540
+ convertOwnerPksForPivotQuery(owners, ownerMeta) {
1541
+ const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
1542
+ const needsConversion = !!pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
1543
+ let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
1544
+ /* v8 ignore next 4 - custom-type PK conversion, tested via loadFromPivotTable path */
1545
+ if (needsConversion) {
1546
+ ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
1547
+ }
1548
+ return { ownerPks, needsConversion, pkProp };
1549
+ }
1367
1550
  getPivotOrderBy(prop, pivotProp, orderBy, parentOrderBy) {
1368
1551
  if (!Utils.isEmpty(orderBy) || RawQueryFragment.hasObjectFragments(orderBy)) {
1369
1552
  return Utils.asArray(orderBy).map(o => ({ [pivotProp.name]: o }));
@@ -1430,6 +1613,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
1430
1613
  if (ref && [ReferenceKind.ONE_TO_ONE, ReferenceKind.MANY_TO_ONE].includes(prop.kind)) {
1431
1614
  return true;
1432
1615
  }
1616
+ // Union-target polymorphic M:N cannot be loaded via a single JOIN because rows span multiple
1617
+ // target tables; fall through to SELECT_IN which dispatches through `loadFromPivotTable`.
1618
+ // Polymorphic M:1 (to-one with target_type discriminator) is handled via LEFT JOINs elsewhere.
1619
+ if (prop.kind === ReferenceKind.MANY_TO_MANY && QueryHelper.isUnionTargetPolymorphic(prop) && prop.owner) {
1620
+ return false;
1621
+ }
1433
1622
  // skip redundant joins for 1:1 owner population hints when using `mapToPk`
1434
1623
  if (prop.kind === ReferenceKind.ONE_TO_ONE && prop.mapToPk && prop.owner) {
1435
1624
  return false;
@@ -1544,6 +1733,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
1544
1733
  const targetPath = `${pathPrefix}${basePath}[${targetMeta.className}]`;
1545
1734
  const schema = targetMeta.schema === '*' ? (options?.schema ?? this.config.get('schema')) : targetMeta.schema;
1546
1735
  qb.addPolymorphicJoin(prop, targetMeta, options.parentTableAlias, tableAlias, JoinType.leftJoin, targetPath, schema);
1736
+ // For polymorphic targets that are TPT child entities, INNER JOIN parent tables so that
1737
+ // filter conditions referencing parent-table columns resolve to the correct alias. The
1738
+ // INNER JOINs get nested inside the polymorphic LEFT JOIN by processNestedJoins, which
1739
+ // keeps the resulting query valid for rows pointing to other polymorphic targets.
1740
+ if (targetMeta.inheritanceType === 'tpt' && targetMeta.tptParent) {
1741
+ this.addTPTParentJoinsForRelation(qb, targetMeta, tableAlias, targetPath);
1742
+ }
1547
1743
  // For polymorphic targets that are TPT base classes, also LEFT JOIN
1548
1744
  // all descendant tables so child-specific fields can be selected.
1549
1745
  if (targetMeta.inheritanceType === 'tpt' && targetMeta.tptChildren?.length && !ref) {
@@ -1592,17 +1788,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1592
1788
  qb.join(field, tableAlias, {}, joinType, path, schema);
1593
1789
  // For relations to TPT child entities, INNER JOIN parent tables (GH #7469)
1594
1790
  if (meta2.inheritanceType === 'tpt' && meta2.tptParent) {
1595
- let childAlias = tableAlias;
1596
- let childMeta = meta2;
1597
- while (childMeta.tptParent) {
1598
- const parentMeta = childMeta.tptParent;
1599
- const parentAlias = qb.getNextAlias(parentMeta.className);
1600
- qb.createAlias(parentMeta.class, parentAlias);
1601
- qb.state.tptAlias[`${tableAlias}:${parentMeta.className}`] = parentAlias;
1602
- qb.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${path}.[tpt]${childMeta.className}`);
1603
- childAlias = parentAlias;
1604
- childMeta = parentMeta;
1605
- }
1791
+ this.addTPTParentJoinsForRelation(qb, meta2, tableAlias, path);
1606
1792
  }
1607
1793
  // For relations to TPT base classes, add LEFT JOINs for all child tables (polymorphic loading)
1608
1794
  if (meta2.inheritanceType === 'tpt' && meta2.tptChildren?.length && !ref) {
@@ -1650,6 +1836,25 @@ export class AbstractSqlDriver extends DatabaseDriver {
1650
1836
  }
1651
1837
  return fields;
1652
1838
  }
1839
+ /**
1840
+ * Walks the TPT inheritance chain of `leafMeta` and INNER JOINs each parent table.
1841
+ * Registers the parent aliases in `qb.state.tptAlias` so column resolution finds them
1842
+ * when filter conditions reference parent-table columns.
1843
+ * @internal
1844
+ */
1845
+ addTPTParentJoinsForRelation(qb, leafMeta, leafAlias, basePath) {
1846
+ let childAlias = leafAlias;
1847
+ let childMeta = leafMeta;
1848
+ while (childMeta.tptParent) {
1849
+ const parentMeta = childMeta.tptParent;
1850
+ const parentAlias = qb.getNextAlias(parentMeta.className);
1851
+ qb.createAlias(parentMeta.class, parentAlias);
1852
+ qb.state.tptAlias[`${leafAlias}:${parentMeta.className}`] = parentAlias;
1853
+ qb.addPropertyJoin(childMeta.tptParentProp, childAlias, parentAlias, JoinType.innerJoin, `${basePath}.[tpt]${childMeta.className}`);
1854
+ childAlias = parentAlias;
1855
+ childMeta = parentMeta;
1856
+ }
1857
+ }
1653
1858
  /**
1654
1859
  * Adds LEFT JOINs and fields for TPT polymorphic loading when populating a relation to a TPT base class.
1655
1860
  * @internal
@@ -1761,18 +1966,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
1761
1966
  });
1762
1967
  }
1763
1968
  const aliased = this.platform.quoteIdentifier(`${tableAlias}__${prop.fieldNames[0]}`);
1969
+ const sourceAlias = qb.helper.getTPTAliasForProperty(prop.name, tableAlias);
1764
1970
  if (prop.customTypes?.some(type => !!type?.convertToJSValueSQL)) {
1765
1971
  return prop.fieldNames.map((col, idx) => {
1766
1972
  if (!prop.customTypes[idx]?.convertToJSValueSQL) {
1767
1973
  return col;
1768
1974
  }
1769
- const prefixed = this.platform.quoteIdentifier(`${tableAlias}.${col}`);
1975
+ const prefixed = this.platform.quoteIdentifier(`${sourceAlias}.${col}`);
1770
1976
  const aliased = this.platform.quoteIdentifier(`${tableAlias}__${col}`);
1771
1977
  return raw(`${prop.customTypes[idx].convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`);
1772
1978
  });
1773
1979
  }
1774
1980
  if (prop.customType?.convertToJSValueSQL) {
1775
- const prefixed = this.platform.quoteIdentifier(`${tableAlias}.${prop.fieldNames[0]}`);
1981
+ const prefixed = this.platform.quoteIdentifier(`${sourceAlias}.${prop.fieldNames[0]}`);
1776
1982
  return [raw(`${prop.customType.convertToJSValueSQL(prefixed, this.platform)} as ${aliased}`)];
1777
1983
  }
1778
1984
  if (prop.formula) {
@@ -1781,7 +1987,6 @@ export class AbstractSqlDriver extends DatabaseDriver {
1781
1987
  const columns = meta.createColumnMappingObject(tableAlias);
1782
1988
  return [raw(`${this.evaluateFormula(prop.formula, columns, table)} as ${aliased}`)];
1783
1989
  }
1784
- const sourceAlias = qb.helper.getTPTAliasForProperty(prop.name, tableAlias);
1785
1990
  return prop.fieldNames.map(fieldName => {
1786
1991
  return raw('?? as ??', [`${sourceAlias}.${fieldName}`, `${tableAlias}__${fieldName}`]);
1787
1992
  });
@@ -1798,6 +2003,57 @@ export class AbstractSqlDriver extends DatabaseDriver {
1798
2003
  }
1799
2004
  return qb;
1800
2005
  }
2006
+ /**
2007
+ * Renders a `FilterQuery` predicate into a SQL fragment (without the `WHERE` keyword and
2008
+ * without table-alias prefixes) suitable for inlining into a partial-index DDL statement.
2009
+ * Used by `DatabaseTable.addIndex` when the user passes an object `where` on `@Index` /
2010
+ * `@Unique`. Strings are returned unchanged.
2011
+ */
2012
+ renderPartialIndexWhere(entityName, where) {
2013
+ if (typeof where === 'string') {
2014
+ return where;
2015
+ }
2016
+ const name = Utils.className(entityName);
2017
+ if (where == null || (Utils.isPlainObject(where) && Object.keys(where).length === 0)) {
2018
+ throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` is empty.`);
2019
+ }
2020
+ const alias = '__p';
2021
+ const qb = this.createQueryBuilder(entityName, undefined, undefined, undefined, undefined, alias);
2022
+ qb.where(where);
2023
+ const sql = qb.getFormattedQuery();
2024
+ // Relation traversal produces join clauses whose aliased identifiers can't be inlined
2025
+ // into a CREATE INDEX ... WHERE clause — reject with a clear error rather than emitting broken DDL.
2026
+ if (/\bjoin\b/i.test(sql.split(/\bwhere\b/i)[0])) {
2027
+ throw new Error(`Cannot render partial-index predicate for entity '${name}': \`where\` may not traverse relations.`);
2028
+ }
2029
+ // Anchor at end-of-string only — the synthetic QB has no top-level order by / limit /
2030
+ // group by / having / offset, so any such keyword inside the captured predicate is
2031
+ // inside a subquery and must not terminate the match.
2032
+ const match = /\bwhere\s+([\s\S]+)$/i.exec(sql);
2033
+ if (!match) {
2034
+ throw new Error(`Failed to render partial-index predicate for entity '${name}': ${sql}`);
2035
+ }
2036
+ const quote = (s) => this.platform.quoteIdentifier(s);
2037
+ const aliasPrefix = new RegExp(`${quote(alias).replace(/[[\]]/g, '\\$&')}\\.`, 'g');
2038
+ const stripped = match[1].replace(aliasPrefix, '').trim();
2039
+ // Any qualified column reference remaining after the alias strip points at another table or
2040
+ // subquery and can't be inlined into a CREATE INDEX ... WHERE predicate. Covers both
2041
+ // QB-generated sub-aliases (quoted, e.g. `"e0"."col"`) and raw fragments with bare refs
2042
+ // (e.g. `raw('other_table.col = 1')`). String literals are erased first so dots inside
2043
+ // them (e.g. JSON path operands like `'$.path'`) don't trip the guard.
2044
+ // Both patterns use a `(?!\s*\()` lookahead so schema-qualified function calls
2045
+ // (`pg_catalog.lower(name)`, `"public".my_func(col)`) are accepted — only `<id>.<id>` not
2046
+ // followed by `(` is treated as a cross-table column reference.
2047
+ const withoutStrings = stripped.replace(/'(?:[^']|'')*'/g, "''");
2048
+ const quotedIdent = String.raw `(?:"(?:[^"]|"")+"|\`(?:[^\`]|\`\`)+\`|\[(?:[^\]]|\]\])+\])`;
2049
+ const anyIdent = `(?:${quotedIdent}|[A-Za-z_]\\w*)`;
2050
+ const quotedCrossRef = new RegExp(`${quotedIdent}\\s*\\.\\s*${anyIdent}(?!\\s*\\()`);
2051
+ const bareCrossRef = /\b[A-Za-z_]\w*\s*\.\s*[A-Za-z_]\w*\b(?!\s*\()/;
2052
+ if (quotedCrossRef.test(withoutStrings) || bareCrossRef.test(withoutStrings)) {
2053
+ 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.`);
2054
+ }
2055
+ return stripped;
2056
+ }
1801
2057
  resolveConnectionType(args) {
1802
2058
  if (args.ctx) {
1803
2059
  return 'write';
@@ -1824,7 +2080,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1824
2080
  for (const prop of meta.relations) {
1825
2081
  if (collections[prop.name]) {
1826
2082
  const pivotMeta = this.metadata.get(prop.pivotEntity);
1827
- const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext);
2083
+ const persister = new PivotCollectionPersister(pivotMeta, this, options?.ctx, options?.schema, options?.loggerContext, pickAbortOptions(options));
1828
2084
  persister.enqueueUpdate(prop, collections[prop.name], clear, pks);
1829
2085
  await this.rethrow(persister.execute());
1830
2086
  }
@@ -1833,6 +2089,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1833
2089
  async lockPessimistic(entity, options) {
1834
2090
  const meta = helper(entity).__meta;
1835
2091
  const qb = this.createQueryBuilder(meta.class, options.ctx, undefined, undefined, options.logging).withSchema(options.schema ?? meta.schema);
2092
+ qb.setAbortOptions(pickAbortOptions(options));
1836
2093
  const cond = Utils.getPrimaryKeyCond(entity, meta.primaryKeys);
1837
2094
  qb.select(raw('1'))
1838
2095
  .where(cond)
@@ -30,7 +30,8 @@ export declare abstract class AbstractSqlPlatform extends Platform {
30
30
  getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | RawQueryFragment;
31
31
  /**
32
32
  * Quotes a key for use inside a JSON path expression (e.g. `$.key`).
33
- * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes.
33
+ * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes
34
+ * with embedded `\` and `"` escaped per the JSON path string syntax.
34
35
  * @internal
35
36
  */
36
37
  quoteJsonKey(key: string): string;
@@ -50,8 +51,19 @@ export declare abstract class AbstractSqlPlatform extends Platform {
50
51
  * @internal
51
52
  */
52
53
  quoteCollation(collation: string): string;
53
- /** @internal */
54
- 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;
55
67
  /** @internal */
56
68
  validateJsonPropertyName(name: string): void;
57
69
  /**