@mikro-orm/sql 7.0.0-rc.2 → 7.0.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 (66) hide show
  1. package/AbstractSqlConnection.d.ts +5 -4
  2. package/AbstractSqlConnection.js +20 -6
  3. package/AbstractSqlDriver.d.ts +19 -13
  4. package/AbstractSqlDriver.js +225 -47
  5. package/AbstractSqlPlatform.d.ts +35 -0
  6. package/AbstractSqlPlatform.js +51 -5
  7. package/PivotCollectionPersister.d.ts +2 -11
  8. package/PivotCollectionPersister.js +59 -59
  9. package/README.md +5 -4
  10. package/SqlEntityManager.d.ts +2 -2
  11. package/SqlEntityManager.js +5 -5
  12. package/dialects/index.d.ts +1 -0
  13. package/dialects/index.js +1 -0
  14. package/dialects/mssql/MsSqlNativeQueryBuilder.d.ts +2 -0
  15. package/dialects/mssql/MsSqlNativeQueryBuilder.js +8 -4
  16. package/dialects/mysql/BaseMySqlPlatform.d.ts +6 -0
  17. package/dialects/mysql/BaseMySqlPlatform.js +18 -2
  18. package/dialects/mysql/MySqlSchemaHelper.d.ts +1 -1
  19. package/dialects/mysql/MySqlSchemaHelper.js +25 -14
  20. package/dialects/oracledb/OracleDialect.d.ts +78 -0
  21. package/dialects/oracledb/OracleDialect.js +166 -0
  22. package/dialects/oracledb/OracleNativeQueryBuilder.d.ts +19 -0
  23. package/dialects/oracledb/OracleNativeQueryBuilder.js +249 -0
  24. package/dialects/oracledb/index.d.ts +2 -0
  25. package/dialects/oracledb/index.js +2 -0
  26. package/dialects/postgresql/BasePostgreSqlPlatform.d.ts +6 -0
  27. package/dialects/postgresql/BasePostgreSqlPlatform.js +49 -37
  28. package/dialects/postgresql/PostgreSqlSchemaHelper.js +75 -59
  29. package/dialects/sqlite/BaseSqliteConnection.js +2 -2
  30. package/dialects/sqlite/NodeSqliteDialect.js +3 -1
  31. package/dialects/sqlite/SqlitePlatform.d.ts +1 -0
  32. package/dialects/sqlite/SqlitePlatform.js +7 -1
  33. package/dialects/sqlite/SqliteSchemaHelper.js +23 -17
  34. package/index.d.ts +1 -1
  35. package/index.js +0 -1
  36. package/package.json +30 -30
  37. package/plugin/index.d.ts +1 -14
  38. package/plugin/index.js +13 -13
  39. package/plugin/transformer.d.ts +6 -22
  40. package/plugin/transformer.js +91 -82
  41. package/query/ArrayCriteriaNode.d.ts +1 -1
  42. package/query/CriteriaNode.js +28 -10
  43. package/query/CriteriaNodeFactory.js +20 -4
  44. package/query/NativeQueryBuilder.d.ts +28 -3
  45. package/query/NativeQueryBuilder.js +65 -3
  46. package/query/ObjectCriteriaNode.js +75 -31
  47. package/query/QueryBuilder.d.ts +199 -100
  48. package/query/QueryBuilder.js +544 -358
  49. package/query/QueryBuilderHelper.d.ts +18 -14
  50. package/query/QueryBuilderHelper.js +364 -147
  51. package/query/ScalarCriteriaNode.js +17 -8
  52. package/query/enums.d.ts +2 -0
  53. package/query/enums.js +2 -0
  54. package/query/raw.js +1 -1
  55. package/schema/DatabaseSchema.d.ts +7 -5
  56. package/schema/DatabaseSchema.js +68 -45
  57. package/schema/DatabaseTable.d.ts +8 -6
  58. package/schema/DatabaseTable.js +191 -107
  59. package/schema/SchemaComparator.d.ts +1 -3
  60. package/schema/SchemaComparator.js +76 -50
  61. package/schema/SchemaHelper.d.ts +2 -13
  62. package/schema/SchemaHelper.js +30 -9
  63. package/schema/SqlSchemaGenerator.d.ts +4 -14
  64. package/schema/SqlSchemaGenerator.js +26 -12
  65. package/typings.d.ts +10 -5
  66. package/tsconfig.build.tsbuildinfo +0 -1
@@ -37,7 +37,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
37
37
  throw new Error('Collation option for SQL drivers must be a string (collation name). Use a CollationOptions object only with MongoDB.');
38
38
  }
39
39
  if (options.indexHint != null && typeof options.indexHint !== 'string') {
40
- throw new Error('indexHint for SQL drivers must be a string (e.g. \'force index(my_index)\'). Use an object only with MongoDB.');
40
+ throw new Error("indexHint for SQL drivers must be a string (e.g. 'force index(my_index)'). Use an object only with MongoDB.");
41
41
  }
42
42
  }
43
43
  createEntityManager(useContext) {
@@ -60,7 +60,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
60
60
  this.validateSqlOptions(options);
61
61
  const { first, last, before, after } = options;
62
62
  const isCursorPagination = [first, last, before, after].some(v => v != null);
63
- qb.__populateWhere = options._populateWhere;
63
+ qb.state.resolvedPopulateWhere = options._populateWhere;
64
64
  qb.select(fields)
65
65
  // only add populateWhere if we are populate-joining, as this will be used to add `on` conditions
66
66
  .populate(populate, joinedProps.length > 0 ? populateWhere : undefined, joinedProps.length > 0 ? options.populateFilter : undefined)
@@ -95,6 +95,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
95
95
  if (meta.virtual) {
96
96
  return this.findVirtual(entityName, where, options);
97
97
  }
98
+ if (options.unionWhere?.length) {
99
+ where = await this.applyUnionWhere(meta, where, options);
100
+ }
98
101
  const qb = await this.createQueryBuilderFromOptions(meta, where, options);
99
102
  const result = await this.rethrow(qb.execute('all'));
100
103
  if (options.last && !options.first) {
@@ -197,7 +200,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
197
200
  if (type === QueryType.COUNT) {
198
201
  native.clear('select').clear('limit').clear('offset').count();
199
202
  }
200
- native.from(raw(`(${expression}) as ${this.platform.quoteIdentifier(qb.alias)}`));
203
+ const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
204
+ native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
201
205
  const query = native.compile();
202
206
  const res = await this.execute(query.sql, query.params, 'all', options.ctx);
203
207
  if (type === QueryType.COUNT) {
@@ -212,7 +216,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
212
216
  const qb = await this.createQueryBuilderFromOptions(meta, where, this.forceBalancedStrategy(options));
213
217
  qb.unsetFlag(QueryFlag.DISABLE_PAGINATE);
214
218
  const native = qb.getNativeQuery(false);
215
- native.from(raw(`(${expression}) as ${this.platform.quoteIdentifier(qb.alias)}`));
219
+ const asKeyword = this.platform.usesAsKeyword() ? ' as ' : ' ';
220
+ native.from(raw(`(${expression})${asKeyword}${this.platform.quoteIdentifier(qb.alias)}`));
216
221
  const query = native.compile();
217
222
  const connectionType = this.resolveConnectionType({ ctx: options.ctx, connectionType: options.connectionType });
218
223
  const res = this.getConnection(connectionType).stream(query.sql, query.params, options.ctx, options.loggerContext);
@@ -265,7 +270,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
265
270
  * and need to be renamed back to `column_name` for the result mapper to work.
266
271
  */
267
272
  mapTPTColumns(result, meta, qb) {
268
- const tptAliases = qb._tptAlias;
273
+ const tptAliases = qb.state.tptAlias;
269
274
  // Walk up the TPT hierarchy
270
275
  let parentMeta = meta.tptParent;
271
276
  while (parentMeta) {
@@ -297,7 +302,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
297
302
  }
298
303
  // Polymorphic to-one: iterate targets, find the matching one, build entity from its columns.
299
304
  // Skip :ref hints — no JOINs were created, so the FK reference is already set by the result mapper.
300
- if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
305
+ if (prop.polymorphic &&
306
+ prop.polymorphTargets?.length &&
307
+ !ref &&
308
+ [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
301
309
  const basePath = parentJoinPath ? `${parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
302
310
  const pathPrefix = !parentJoinPath ? '[populate]' : '';
303
311
  let matched = false;
@@ -306,16 +314,22 @@ export class AbstractSqlDriver extends DatabaseDriver {
306
314
  const relationAlias = qb.getAliasForJoinPath(targetPath, { matchPopulateJoins: true });
307
315
  const meta2 = targetMeta;
308
316
  const targetProps = meta2.props.filter(p => this.platform.shouldHaveColumn(p, hint.children || []));
309
- const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => root[`${relationAlias}__${name}`] != null));
317
+ const hasPK = meta2
318
+ .getPrimaryProps()
319
+ .every(pk => pk.fieldNames.every(name => root[`${relationAlias}__${name}`] != null));
310
320
  if (hasPK && !matched) {
311
321
  matched = true;
312
- let relationPojo = {};
322
+ const relationPojo = {};
313
323
  const tz = this.platform.getTimezone();
314
324
  for (const p of targetProps) {
315
325
  this.mapJoinedProp(relationPojo, p, relationAlias, root, tz, meta2);
316
326
  }
317
327
  // Inject the entity class constructor so that the factory creates the correct type
318
- Object.defineProperty(relationPojo, 'constructor', { value: meta2.class, enumerable: false, configurable: true });
328
+ Object.defineProperty(relationPojo, 'constructor', {
329
+ value: meta2.class,
330
+ enumerable: false,
331
+ configurable: true,
332
+ });
319
333
  result[prop.name] = relationPojo;
320
334
  const populateChildren = hint.children || [];
321
335
  this.mapJoinedProps(relationPojo, meta2, populateChildren, qb, root, map, targetPath);
@@ -366,7 +380,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
366
380
  return;
367
381
  }
368
382
  const mapToPk = !hint.dataOnly && !!(ref || prop.mapToPk);
369
- const targetProps = mapToPk ? meta2.getPrimaryProps() : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
383
+ const targetProps = mapToPk
384
+ ? meta2.getPrimaryProps()
385
+ : meta2.props.filter(prop => this.platform.shouldHaveColumn(prop, hint.children || []));
370
386
  // If the primary key value for the relation is null, we know we haven't joined to anything
371
387
  // and therefore we don't return any record (since all values would be null)
372
388
  const hasPK = meta2.getPrimaryProps().every(pk => pk.fieldNames.every(name => {
@@ -459,7 +475,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
459
475
  else if (prop.runtimeType === 'Date') {
460
476
  const alias = `${relationAlias}__${prop.fieldNames[0]}`;
461
477
  const value = root[alias];
462
- if (tz && tz !== 'local' && typeof value === 'string' && !value.includes('+') && value.lastIndexOf('-') < 11 && !value.endsWith('Z')) {
478
+ if (tz &&
479
+ tz !== 'local' &&
480
+ typeof value === 'string' &&
481
+ !value.includes('+') &&
482
+ value.lastIndexOf('-') < 11 &&
483
+ !value.endsWith('Z')) {
463
484
  relationPojo[prop.name] = this.platform.parseDate(value + tz);
464
485
  }
465
486
  else if (['string', 'number'].includes(typeof value)) {
@@ -475,10 +496,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
475
496
  if (prop.kind === ReferenceKind.EMBEDDED && (prop.object || meta.embeddable)) {
476
497
  const item = parseJsonSafe(relationPojo[prop.name]);
477
498
  if (Array.isArray(item)) {
478
- relationPojo[prop.name] = item.map(row => (row == null ? row : this.comparator.mapResult(prop.targetMeta, row)));
499
+ relationPojo[prop.name] = item.map(row => row == null ? row : this.comparator.mapResult(prop.targetMeta, row));
479
500
  }
480
501
  else {
481
- relationPojo[prop.name] = item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
502
+ relationPojo[prop.name] =
503
+ item == null ? item : this.comparator.mapResult(prop.targetMeta, item);
482
504
  }
483
505
  }
484
506
  }
@@ -493,6 +515,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
493
515
  if (meta.virtual) {
494
516
  return this.countVirtual(entityName, where, options);
495
517
  }
518
+ if (options.unionWhere?.length) {
519
+ where = await this.applyUnionWhere(meta, where, options);
520
+ }
496
521
  options = { populate: [], ...options };
497
522
  const populate = options.populate;
498
523
  const joinedProps = this.joinedProps(meta, populate, options);
@@ -503,7 +528,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
503
528
  this.buildFields(meta, populate, joinedProps, qb, qb.alias, options, schema);
504
529
  }
505
530
  this.validateSqlOptions(options);
506
- qb.__populateWhere = options._populateWhere;
531
+ qb.state.resolvedPopulateWhere = options._populateWhere;
507
532
  qb.indexHint(options.indexHint)
508
533
  .collation(options.collation)
509
534
  .comment(options.comments)
@@ -533,7 +558,12 @@ export class AbstractSqlDriver extends DatabaseDriver {
533
558
  else {
534
559
  /* v8 ignore next */
535
560
  res.insertId = data[meta.primaryKeys[0]] ?? res.insertId ?? res.row[meta.primaryKeys[0]];
536
- pk = [res.insertId];
561
+ if (options.convertCustomTypes && meta?.getPrimaryProp().customType) {
562
+ pk = [meta.getPrimaryProp().customType.convertToDatabaseValue(res.insertId, this.platform)];
563
+ }
564
+ else {
565
+ pk = [res.insertId];
566
+ }
537
567
  }
538
568
  await this.processManyToMany(meta, pk, collections, false, options);
539
569
  return res;
@@ -557,13 +587,19 @@ export class AbstractSqlDriver extends DatabaseDriver {
557
587
  }
558
588
  const tableName = this.getTableName(meta, options);
559
589
  let sql = `insert into ${tableName} `;
560
- sql += fields.length > 0 ? '(' + fields.map(k => this.platform.quoteIdentifier(k)).join(', ') + ')' : `(${this.platform.quoteIdentifier(pks[0])})`;
590
+ sql +=
591
+ fields.length > 0
592
+ ? '(' + fields.map(k => this.platform.quoteIdentifier(k)).join(', ') + ')'
593
+ : `(${this.platform.quoteIdentifier(pks[0])})`;
561
594
  if (this.platform.usesOutputStatement()) {
562
595
  const returningProps = this.getTableProps(meta)
563
596
  .filter(prop => (prop.persist !== false && prop.defaultRaw) || prop.autoincrement || prop.generated)
564
597
  .filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
565
598
  const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
566
- sql += returningFields.length > 0 ? ` output ${returningFields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}` : '';
599
+ sql +=
600
+ returningFields.length > 0
601
+ ? ` output ${returningFields.map(field => 'inserted.' + this.platform.quoteIdentifier(field)).join(', ')}`
602
+ : '';
567
603
  }
568
604
  if (fields.length > 0 || this.platform.usesDefaultKeyword()) {
569
605
  sql += ' values ';
@@ -650,7 +686,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
650
686
  else {
651
687
  const field = prop.fieldNames[0];
652
688
  if (!duplicates.includes(field) || !usedDups.includes(field)) {
653
- if (prop.customType && !prop.object && 'convertToDatabaseValueSQL' in prop.customType && row[prop.name] != null && !isRaw(row[prop.name])) {
689
+ if (prop.customType &&
690
+ !prop.object &&
691
+ 'convertToDatabaseValueSQL' in prop.customType &&
692
+ row[prop.name] != null &&
693
+ !isRaw(row[prop.name])) {
654
694
  keys.push(prop.customType.convertToDatabaseValueSQL('?', this.platform));
655
695
  }
656
696
  else {
@@ -671,7 +711,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
671
711
  .filter(prop => !(prop.name in data[0]) || isRaw(data[0][prop.name]));
672
712
  const returningFields = Utils.flatten(returningProps.map(prop => prop.fieldNames));
673
713
  /* v8 ignore next */
674
- sql += returningFields.length > 0 ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}` : '';
714
+ sql +=
715
+ returningFields.length > 0
716
+ ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}`
717
+ : '';
675
718
  }
676
719
  if (transform) {
677
720
  sql = transform(sql);
@@ -704,11 +747,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
704
747
  /* v8 ignore next */
705
748
  where = { [meta.primaryKeys[0] ?? pks[0]]: where };
706
749
  }
750
+ if (!options.upsert && options.unionWhere?.length) {
751
+ where = (await this.applyUnionWhere(meta, where, options, true));
752
+ }
707
753
  if (Utils.hasObjectKeys(data)) {
708
754
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
709
755
  if (options.upsert) {
710
756
  /* v8 ignore next */
711
- const uniqueFields = options.onConflictFields ?? (Utils.isPlainObject(where) ? Utils.keys(where) : meta.primaryKeys);
757
+ const uniqueFields = options.onConflictFields ??
758
+ (Utils.isPlainObject(where) ? Utils.keys(where) : meta.primaryKeys);
712
759
  const returning = getOnConflictReturningFields(meta, data, uniqueFields, options);
713
760
  qb.insert(data)
714
761
  .onConflict(uniqueFields)
@@ -728,7 +775,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
728
775
  qb.update(data).where(where);
729
776
  // reload generated columns and version fields
730
777
  const returning = [];
731
- meta.props.filter(prop => (prop.generated && !prop.primary) || prop.version).forEach(prop => returning.push(prop.name));
778
+ meta.props
779
+ .filter(prop => (prop.generated && !prop.primary) || prop.version)
780
+ .forEach(prop => returning.push(prop.name));
732
781
  qb.returning(returning);
733
782
  }
734
783
  res = await this.rethrow(qb.execute('run', false));
@@ -738,13 +787,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
738
787
  await this.processManyToMany(meta, pk, collections, true, options);
739
788
  return res;
740
789
  }
741
- async nativeUpdateMany(entityName, where, data, options = {}) {
790
+ async nativeUpdateMany(entityName, where, data, options = {}, transform) {
742
791
  options.processCollections ??= true;
743
792
  options.convertCustomTypes ??= true;
744
793
  const meta = this.metadata.get(entityName);
745
794
  if (options.upsert) {
746
795
  const uniqueFields = options.onConflictFields ??
747
- (Utils.isPlainObject(where[0]) ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key)) : meta.primaryKeys);
796
+ (Utils.isPlainObject(where[0])
797
+ ? Object.keys(where[0]).flatMap(key => Utils.splitPrimaryKeys(key))
798
+ : meta.primaryKeys);
748
799
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
749
800
  const returning = getOnConflictReturningFields(meta, data[0], uniqueFields, options);
750
801
  qb.insert(data)
@@ -815,7 +866,11 @@ export class AbstractSqlDriver extends DatabaseDriver {
815
866
  if (key in data[idx]) {
816
867
  const pks = Utils.getOrderedPrimaryKeys(cond, meta);
817
868
  sql += ` when (${pkCond}) then `;
818
- if (prop.customType && !prop.object && 'convertToDatabaseValueSQL' in prop.customType && data[idx][prop.name] != null && !isRaw(data[idx][key])) {
869
+ if (prop.customType &&
870
+ !prop.object &&
871
+ 'convertToDatabaseValueSQL' in prop.customType &&
872
+ data[idx][prop.name] != null &&
873
+ !isRaw(data[idx][key])) {
819
874
  sql += prop.customType.convertToDatabaseValueSQL('?', this.platform);
820
875
  }
821
876
  else {
@@ -844,7 +899,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
844
899
  sql = sql.substring(0, sql.length - 2) + ' where ';
845
900
  const pkProps = meta.primaryKeys.concat(...meta.concurrencyCheckKeys);
846
901
  const pks = Utils.flatten(pkProps.map(pk => meta.properties[pk].fieldNames));
847
- sql += pks.length > 1 ? `(${pks.map(pk => `${this.platform.quoteIdentifier(pk)}`).join(', ')})` : this.platform.quoteIdentifier(pks[0]);
902
+ sql +=
903
+ pks.length > 1
904
+ ? `(${pks.map(pk => this.platform.quoteIdentifier(pk)).join(', ')})`
905
+ : this.platform.quoteIdentifier(pks[0]);
848
906
  const conds = where.map(cond => {
849
907
  if (Utils.isPlainObject(cond) && Utils.getObjectKeysSize(cond) === 1) {
850
908
  cond = Object.values(cond)[0];
@@ -867,7 +925,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
867
925
  if (this.platform.usesReturningStatement() && returning.size > 0) {
868
926
  const returningFields = Utils.flatten([...returning].map(prop => meta.properties[prop].fieldNames));
869
927
  /* v8 ignore next */
870
- sql += returningFields.length > 0 ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}` : '';
928
+ sql +=
929
+ returningFields.length > 0
930
+ ? ` returning ${returningFields.map(field => this.platform.quoteIdentifier(field)).join(', ')}`
931
+ : '';
932
+ }
933
+ if (transform) {
934
+ sql = transform(sql, params);
871
935
  }
872
936
  const res = await this.rethrow(this.execute(sql, params, 'run', options.ctx, options.loggerContext));
873
937
  for (let i = 0; i < collections.length; i++) {
@@ -881,6 +945,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
881
945
  if (Utils.isPrimaryKey(where) && pks.length === 1) {
882
946
  where = { [pks[0]]: where };
883
947
  }
948
+ if (options.unionWhere?.length) {
949
+ where = await this.applyUnionWhere(meta, where, options, true);
950
+ }
884
951
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
885
952
  .delete(where)
886
953
  .withSchema(this.getSchemaName(meta, options));
@@ -935,7 +1002,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
935
1002
  const qb = this.createQueryBuilder(coll.property.targetMeta.class, options?.ctx, 'write').withSchema(this.getSchemaName(meta, options));
936
1003
  if (coll.getSnapshot() === undefined) {
937
1004
  if (coll.property.orphanRemoval) {
938
- const query = qb.delete({ [coll.property.mappedBy]: pks }).andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
1005
+ const query = qb
1006
+ .delete({ [coll.property.mappedBy]: pks })
1007
+ .andWhere({ [cols.join(Utils.PK_SEPARATOR)]: { $nin: insertDiff } });
939
1008
  await this.rethrow(query.execute());
940
1009
  continue;
941
1010
  }
@@ -947,7 +1016,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
947
1016
  continue;
948
1017
  }
949
1018
  /* v8 ignore next */
950
- const query = qb.update({ [coll.property.mappedBy]: pks }).where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
1019
+ const query = qb
1020
+ .update({ [coll.property.mappedBy]: pks })
1021
+ .where({ [cols.join(Utils.PK_SEPARATOR)]: { $in: insertDiff } });
951
1022
  await this.rethrow(query.execute());
952
1023
  continue;
953
1024
  }
@@ -960,7 +1031,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
960
1031
  else {
961
1032
  const targetMeta = coll.property.targetMeta;
962
1033
  const targetSchema = (coll[0] ?? snap?.[0]) && helper(coll[0] ?? snap?.[0]).getSchema();
963
- schema = targetMeta.schema === '*' ? (options?.schema ?? targetSchema ?? this.config.get('schema')) : targetMeta.schema;
1034
+ schema =
1035
+ targetMeta.schema === '*'
1036
+ ? (options?.schema ?? targetSchema ?? this.config.get('schema'))
1037
+ : targetMeta.schema;
964
1038
  }
965
1039
  }
966
1040
  else if (schema == null) {
@@ -986,8 +1060,17 @@ export class AbstractSqlDriver extends DatabaseDriver {
986
1060
  const pivotProp1 = pivotMeta.relations[prop.owner ? 1 : 0];
987
1061
  const pivotProp2 = pivotMeta.relations[prop.owner ? 0 : 1];
988
1062
  const ownerMeta = pivotProp2.targetMeta;
1063
+ // The pivot query builder doesn't convert custom types, so we need to manually
1064
+ // convert owner PKs to DB format for the query and convert result FKs back to
1065
+ // JS format for consistent key hashing in buildPivotResultMap.
1066
+ const pkProp = ownerMeta.properties[ownerMeta.primaryKeys[0]];
1067
+ const needsConversion = pkProp?.customType?.ensureComparable(ownerMeta, pkProp) && !ownerMeta.compositePK;
1068
+ let ownerPks = ownerMeta.compositePK ? owners : owners.map(o => o[0]);
1069
+ if (needsConversion) {
1070
+ ownerPks = ownerPks.map(v => pkProp.customType.convertToDatabaseValue(v, this.platform, { mode: 'query' }));
1071
+ }
989
1072
  const cond = {
990
- [pivotProp2.name]: { $in: ownerMeta.compositePK ? owners : owners.map(o => o[0]) },
1073
+ [pivotProp2.name]: { $in: ownerPks },
991
1074
  };
992
1075
  if (!Utils.isEmpty(where)) {
993
1076
  cond[pivotProp1.name] = { ...where };
@@ -997,7 +1080,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
997
1080
  const populate = this.autoJoinOneToOneOwner(prop.targetMeta, options?.populate ?? [], options?.fields);
998
1081
  const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${pivotProp1.name}.${f}`) : [];
999
1082
  const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${pivotProp1.name}.${f}`) : [];
1000
- const fields = pivotJoin ? [pivotProp1.name, pivotProp2.name] : [pivotProp1.name, pivotProp2.name, ...childFields];
1083
+ const fields = pivotJoin
1084
+ ? [pivotProp1.name, pivotProp2.name]
1085
+ : [pivotProp1.name, pivotProp2.name, ...childFields];
1001
1086
  const res = await this.find(pivotMeta.class, where, {
1002
1087
  ctx,
1003
1088
  ...options,
@@ -1018,6 +1103,16 @@ export class AbstractSqlDriver extends DatabaseDriver {
1018
1103
  _populateWhere: 'infer',
1019
1104
  populateFilter: this.wrapPopulateFilter(options, pivotProp2.name),
1020
1105
  });
1106
+ // Convert result FK values back to JS format so key hashing
1107
+ // in buildPivotResultMap is consistent with the owner keys.
1108
+ if (needsConversion) {
1109
+ for (const item of res) {
1110
+ const fk = item[pivotProp2.name];
1111
+ if (fk != null) {
1112
+ item[pivotProp2.name] = pkProp.customType.convertToJSValue(fk, this.platform);
1113
+ }
1114
+ }
1115
+ }
1021
1116
  return this.buildPivotResultMap(owners, res, pivotProp2.name, pivotProp1.name);
1022
1117
  }
1023
1118
  /**
@@ -1050,7 +1145,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1050
1145
  const populateField = pivotJoin ? `${inverseProp.name}:ref` : inverseProp.name;
1051
1146
  const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
1052
1147
  const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${inverseProp.name}.${f}`) : [];
1053
- const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${inverseProp.name}.${f}`) : [];
1148
+ const childExclude = !Utils.isEmpty(options?.exclude)
1149
+ ? options.exclude.map(f => `${inverseProp.name}.${f}`)
1150
+ : [];
1054
1151
  const fields = pivotJoin
1055
1152
  ? [inverseProp.name, prop.discriminator, prop.discriminatorColumn]
1056
1153
  : [inverseProp.name, prop.discriminator, prop.discriminatorColumn, ...childFields];
@@ -1060,7 +1157,15 @@ export class AbstractSqlDriver extends DatabaseDriver {
1060
1157
  fields,
1061
1158
  exclude: childExclude,
1062
1159
  orderBy: this.getPivotOrderBy(prop, inverseProp, orderBy, options?.orderBy),
1063
- populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate, dataOnly: inverseProp.mapToPk && !pivotJoin }],
1160
+ populate: [
1161
+ {
1162
+ field: populateField,
1163
+ strategy: LoadStrategy.JOINED,
1164
+ joinType: JoinType.innerJoin,
1165
+ children: populate,
1166
+ dataOnly: inverseProp.mapToPk && !pivotJoin,
1167
+ },
1168
+ ],
1064
1169
  populateWhere: undefined,
1065
1170
  // @ts-ignore
1066
1171
  _populateWhere: 'infer',
@@ -1092,7 +1197,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1092
1197
  const populateField = ownerRelationName;
1093
1198
  const populate = this.autoJoinOneToOneOwner(targetMeta, options?.populate ?? [], options?.fields);
1094
1199
  const childFields = !Utils.isEmpty(options?.fields) ? options.fields.map(f => `${ownerRelationName}.${f}`) : [];
1095
- const childExclude = !Utils.isEmpty(options?.exclude) ? options.exclude.map(f => `${ownerRelationName}.${f}`) : [];
1200
+ const childExclude = !Utils.isEmpty(options?.exclude)
1201
+ ? options.exclude.map(f => `${ownerRelationName}.${f}`)
1202
+ : [];
1096
1203
  const fields = [ownerRelationName, tagProp.name, prop.discriminatorColumn, ...childFields];
1097
1204
  const res = await this.find(pivotMeta.class, cond, {
1098
1205
  ctx,
@@ -1100,7 +1207,14 @@ export class AbstractSqlDriver extends DatabaseDriver {
1100
1207
  fields,
1101
1208
  exclude: childExclude,
1102
1209
  orderBy: this.getPivotOrderBy(prop, ownerProp, orderBy, options?.orderBy),
1103
- populate: [{ field: populateField, strategy: LoadStrategy.JOINED, joinType: JoinType.innerJoin, children: populate }],
1210
+ populate: [
1211
+ {
1212
+ field: populateField,
1213
+ strategy: LoadStrategy.JOINED,
1214
+ joinType: JoinType.innerJoin,
1215
+ children: populate,
1216
+ },
1217
+ ],
1104
1218
  populateWhere: undefined,
1105
1219
  // @ts-ignore
1106
1220
  _populateWhere: 'infer',
@@ -1179,7 +1293,10 @@ export class AbstractSqlDriver extends DatabaseDriver {
1179
1293
  }
1180
1294
  const relationsToPopulate = populate.map(({ field }) => field.split(':')[0]);
1181
1295
  const toPopulate = meta.relations
1182
- .filter(prop => prop.kind === ReferenceKind.ONE_TO_ONE && !prop.owner && !prop.lazy && !relationsToPopulate.includes(prop.name))
1296
+ .filter(prop => prop.kind === ReferenceKind.ONE_TO_ONE &&
1297
+ !prop.owner &&
1298
+ !prop.lazy &&
1299
+ !relationsToPopulate.includes(prop.name))
1183
1300
  .filter(prop => fields.length === 0 || fields.some(f => prop.name === f || prop.name.startsWith(`${String(f)}.`)))
1184
1301
  .map(prop => ({ field: `${prop.name}:ref`, strategy: LoadStrategy.JOINED }));
1185
1302
  return [...populate, ...toPopulate];
@@ -1295,8 +1412,13 @@ export class AbstractSqlDriver extends DatabaseDriver {
1295
1412
  const prop = meta.properties[propName];
1296
1413
  // Polymorphic to-one: create a LEFT JOIN per target type
1297
1414
  // Skip :ref hints — polymorphic to-one already has FK + discriminator in the row
1298
- if (prop.polymorphic && prop.polymorphTargets?.length && !ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
1299
- const basePath = options.parentJoinPath ? `${options.parentJoinPath}.${prop.name}` : `${meta.name}.${prop.name}`;
1415
+ if (prop.polymorphic &&
1416
+ prop.polymorphTargets?.length &&
1417
+ !ref &&
1418
+ [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind)) {
1419
+ const basePath = options.parentJoinPath
1420
+ ? `${options.parentJoinPath}.${prop.name}`
1421
+ : `${meta.name}.${prop.name}`;
1300
1422
  const pathPrefix = !options.parentJoinPath && populateWhereAll && !basePath.startsWith('[populate]') ? '[populate]' : '';
1301
1423
  for (const targetMeta of prop.polymorphTargets) {
1302
1424
  const tableAlias = qb.getNextAlias(targetMeta.className);
@@ -1314,7 +1436,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1314
1436
  continue;
1315
1437
  }
1316
1438
  // ignore ref joins of known FKs unless it's a filter hint
1317
- if (ref && !hint.filter && (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))) {
1439
+ if (ref &&
1440
+ !hint.filter &&
1441
+ (prop.kind === ReferenceKind.MANY_TO_ONE || (prop.kind === ReferenceKind.ONE_TO_ONE && prop.owner))) {
1318
1442
  continue;
1319
1443
  }
1320
1444
  const meta2 = prop.targetMeta;
@@ -1360,7 +1484,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1360
1484
  childExplicitFields.push(f.substring(prop.name.length + 1));
1361
1485
  }
1362
1486
  });
1363
- const childExclude = options.exclude ? Utils.extractChildElements(options.exclude, prop.name) : options.exclude;
1487
+ const childExclude = options.exclude
1488
+ ? Utils.extractChildElements(options.exclude, prop.name)
1489
+ : options.exclude;
1364
1490
  if (!ref && (!prop.mapToPk || hint.dataOnly)) {
1365
1491
  fields.push(...this.getFieldsForJoinedLoad(qb, meta2, {
1366
1492
  ...options,
@@ -1371,7 +1497,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1371
1497
  parentJoinPath: path,
1372
1498
  }));
1373
1499
  }
1374
- else if (hint.filter || (prop.mapToPk && !hint.dataOnly) || (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
1500
+ else if (hint.filter ||
1501
+ (prop.mapToPk && !hint.dataOnly) ||
1502
+ (ref && [ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind))) {
1375
1503
  fields.push(...prop.referencedColumnNames.map(col => qb.helper.mapper(`${tableAlias}.${col}`, qb.type, undefined, `${tableAlias}__${col}`)));
1376
1504
  }
1377
1505
  }
@@ -1410,7 +1538,7 @@ export class AbstractSqlDriver extends DatabaseDriver {
1410
1538
  * @internal
1411
1539
  */
1412
1540
  findTPTChildAlias(qb, childMeta) {
1413
- const joins = qb._joins;
1541
+ const joins = qb.state.joins;
1414
1542
  for (const key of Object.keys(joins)) {
1415
1543
  if (joins[key].table === childMeta.tableName && key.includes('[tpt]')) {
1416
1544
  return joins[key].alias;
@@ -1473,7 +1601,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1473
1601
  if (childAlias) {
1474
1602
  // Map fields using same filtering as joined loading, plus skip PKs
1475
1603
  for (const prop of currentMeta.ownProps.filter(p => !p.primary && this.platform.shouldHaveColumn(p, []))) {
1476
- this.mapJoinedProp(relationPojo, prop, childAlias, root, tz, currentMeta, { deleteFromRoot: true });
1604
+ this.mapJoinedProp(relationPojo, prop, childAlias, root, tz, currentMeta, {
1605
+ deleteFromRoot: true,
1606
+ });
1477
1607
  }
1478
1608
  }
1479
1609
  currentMeta = currentMeta.tptParent;
@@ -1522,7 +1652,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1522
1652
  /** @internal */
1523
1653
  createQueryBuilder(entityName, ctx, preferredConnectionType, convertCustomTypes, loggerContext, alias, em) {
1524
1654
  // do not compute the connectionType if EM is provided as it will be computed from it in the QB later on
1525
- const connectionType = em ? preferredConnectionType : this.resolveConnectionType({ ctx, connectionType: preferredConnectionType });
1655
+ const connectionType = em
1656
+ ? preferredConnectionType
1657
+ : this.resolveConnectionType({ ctx, connectionType: preferredConnectionType });
1526
1658
  const qb = new QueryBuilder(entityName, this.metadata, this, ctx, alias, connectionType, em, loggerContext);
1527
1659
  if (!convertCustomTypes) {
1528
1660
  qb.unsetFlag(QueryFlag.CONVERT_CUSTOM_TYPES);
@@ -1598,6 +1730,47 @@ export class AbstractSqlDriver extends DatabaseDriver {
1598
1730
  /* v8 ignore next */
1599
1731
  return { $and: [options.populateWhere, where] };
1600
1732
  }
1733
+ /**
1734
+ * Builds a UNION ALL (or UNION) subquery from `unionWhere` branches and merges it
1735
+ * into the main WHERE as `pk IN (branch_1 UNION ALL branch_2 ...)`.
1736
+ * Each branch is planned independently by the database, enabling per-table index usage.
1737
+ */
1738
+ async applyUnionWhere(meta, where, options, forDml = false) {
1739
+ const unionWhere = options.unionWhere;
1740
+ const strategy = options.unionWhereStrategy ?? 'union-all';
1741
+ const schema = this.getSchemaName(meta, options);
1742
+ const connectionType = this.resolveConnectionType({
1743
+ ctx: options.ctx,
1744
+ connectionType: options.connectionType,
1745
+ });
1746
+ const branchQbs = [];
1747
+ for (const branch of unionWhere) {
1748
+ const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging).withSchema(schema);
1749
+ const pkFields = meta.primaryKeys.map(pk => {
1750
+ const prop = meta.properties[pk];
1751
+ return `${qb.alias}.${prop.fieldNames[0]}`;
1752
+ });
1753
+ qb.select(pkFields).where(branch);
1754
+ if (options.em) {
1755
+ await qb.applyJoinedFilters(options.em, options.filters);
1756
+ }
1757
+ branchQbs.push(qb);
1758
+ }
1759
+ const [first, ...rest] = branchQbs;
1760
+ const unionQb = strategy === 'union' ? first.union(...rest) : first.unionAll(...rest);
1761
+ const pkHash = Utils.getPrimaryKeyHash(meta.primaryKeys);
1762
+ // MySQL does not allow referencing the target table in a subquery
1763
+ // for UPDATE/DELETE, so we wrap the union in a derived table.
1764
+ if (forDml) {
1765
+ const { sql, params } = unionQb.toQuery();
1766
+ return {
1767
+ $and: [where, { [pkHash]: { $in: raw(`select * from (${sql}) as __u`, params) } }],
1768
+ };
1769
+ }
1770
+ return {
1771
+ $and: [where, { [pkHash]: { $in: unionQb.toRaw() } }],
1772
+ };
1773
+ }
1601
1774
  buildOrderBy(qb, meta, populate, options) {
1602
1775
  const joinedProps = this.joinedProps(meta, populate, options);
1603
1776
  // `options._populateWhere` is a copy of the value provided by user with a fallback to the global config option
@@ -1628,7 +1801,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1628
1801
  let path = parentPath;
1629
1802
  const meta2 = prop.targetMeta;
1630
1803
  if (prop.kind !== ReferenceKind.SCALAR &&
1631
- (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) || !prop.owner || Utils.isPlainObject(childOrder))) {
1804
+ (![ReferenceKind.MANY_TO_ONE, ReferenceKind.ONE_TO_ONE].includes(prop.kind) ||
1805
+ !prop.owner ||
1806
+ Utils.isPlainObject(childOrder))) {
1632
1807
  path += `.${field}`;
1633
1808
  }
1634
1809
  if (prop.kind === ReferenceKind.MANY_TO_MANY && typeof childOrder !== 'object') {
@@ -1639,7 +1814,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
1639
1814
  if (!join) {
1640
1815
  continue;
1641
1816
  }
1642
- if (join && ![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind) && typeof childOrder === 'object') {
1817
+ if (join &&
1818
+ ![ReferenceKind.SCALAR, ReferenceKind.EMBEDDED].includes(prop.kind) &&
1819
+ typeof childOrder === 'object') {
1643
1820
  const children = this.buildPopulateOrderBy(qb, meta2, Utils.asArray(childOrder), path, explicit, propAlias);
1644
1821
  orderBy.push(...children);
1645
1822
  continue;
@@ -1770,7 +1947,8 @@ export class AbstractSqlDriver extends DatabaseDriver {
1770
1947
  if (!options.fields.includes('*') && !options.fields.includes(`${qb.alias}.*`)) {
1771
1948
  ret.unshift(...meta.primaryKeys.filter(pk => !options.fields.includes(pk)));
1772
1949
  }
1773
- if (meta.root.inheritanceType === 'sti' && !options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
1950
+ if (meta.root.inheritanceType === 'sti' &&
1951
+ !options.fields.includes(`${qb.alias}.${meta.root.discriminatorColumn}`)) {
1774
1952
  ret.push(meta.root.discriminatorColumn);
1775
1953
  }
1776
1954
  }
@@ -4,6 +4,7 @@ import { type SchemaHelper } from './schema/SchemaHelper.js';
4
4
  import type { IndexDef } from './typings.js';
5
5
  import { NativeQueryBuilder } from './query/NativeQueryBuilder.js';
6
6
  export declare abstract class AbstractSqlPlatform extends Platform {
7
+ #private;
7
8
  protected readonly schemaHelper?: SchemaHelper;
8
9
  usesPivotTable(): boolean;
9
10
  indexForeignKeys(): boolean;
@@ -26,7 +27,14 @@ export declare abstract class AbstractSqlPlatform extends Platform {
26
27
  quoteValue(value: any): string;
27
28
  getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | RawQueryFragment;
28
29
  getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | RawQueryFragment;
30
+ /**
31
+ * Quotes a key for use inside a JSON path expression (e.g. `$.key`).
32
+ * Simple alphanumeric keys are left unquoted; others are wrapped in double quotes.
33
+ * @internal
34
+ */
35
+ quoteJsonKey(key: string): string;
29
36
  getJsonIndexDefinition(index: IndexDef): string[];
37
+ supportsUnionWhere(): boolean;
30
38
  supportsSchemas(): boolean;
31
39
  /** @inheritDoc */
32
40
  generateCustomOrder(escapedColumn: string, values: unknown[]): string;
@@ -41,4 +49,31 @@ export declare abstract class AbstractSqlPlatform extends Platform {
41
49
  quoteCollation(collation: string): string;
42
50
  /** @internal */
43
51
  protected validateCollationName(collation: string): void;
52
+ /** @internal */
53
+ validateJsonPropertyName(name: string): void;
54
+ /**
55
+ * Returns FROM clause for JSON array iteration.
56
+ * @internal
57
+ */
58
+ getJsonArrayFromSQL(column: string, alias: string, _properties: {
59
+ name: string;
60
+ type: string;
61
+ }[]): string;
62
+ /**
63
+ * Returns SQL expression to access an element's property within a JSON array iteration.
64
+ * @internal
65
+ */
66
+ getJsonArrayElementPropertySQL(alias: string, property: string, _type: string): string;
67
+ /**
68
+ * Wraps JSON array FROM clause and WHERE condition into a full EXISTS condition.
69
+ * MySQL overrides this because `json_table` doesn't support correlated subqueries.
70
+ * @internal
71
+ */
72
+ getJsonArrayExistsSQL(from: string, where: string): string;
73
+ /**
74
+ * Maps a runtime type name (e.g. 'string', 'number') to a driver-specific bind type constant.
75
+ * Used by NativeQueryBuilder for output bindings.
76
+ * @internal
77
+ */
78
+ mapToBindType(type: string): unknown;
44
79
  }