@mikro-orm/sql 7.0.0-dev.291 → 7.0.0-dev.293

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.
@@ -137,6 +137,12 @@ export declare abstract class AbstractSqlDriver<Connection extends AbstractSqlCo
137
137
  protected processManyToMany<T extends object>(meta: EntityMetadata<T>, pks: Primary<T>[], collections: EntityData<T>, clear: boolean, options?: DriverMethodOptions): Promise<void>;
138
138
  lockPessimistic<T extends object>(entity: T, options: LockOptions): Promise<void>;
139
139
  protected buildPopulateWhere<T extends object>(meta: EntityMetadata<T>, joinedProps: PopulateOptions<T>[], options: Pick<FindOptions<any>, 'populateWhere'>): ObjectQuery<T>;
140
+ /**
141
+ * Builds a UNION ALL (or UNION) subquery from `unionWhere` branches and merges it
142
+ * into the main WHERE as `pk IN (branch_1 UNION ALL branch_2 ...)`.
143
+ * Each branch is planned independently by the database, enabling per-table index usage.
144
+ */
145
+ protected applyUnionWhere<T extends object>(meta: EntityMetadata<T>, where: ObjectQuery<T>, options: FindOptions<T, any, any, any> | CountOptions<T> | NativeInsertUpdateOptions<T> | DeleteOptions<T>, forDml?: boolean): Promise<ObjectQuery<T>>;
140
146
  protected buildOrderBy<T extends object>(qb: QueryBuilder<T, any, any, any>, meta: EntityMetadata<T>, populate: PopulateOptions<T>[], options: Pick<FindOptions<any>, 'strategy' | 'orderBy' | 'populateOrderBy'>): QueryOrderMap<T>[];
141
147
  protected buildPopulateOrderBy<T extends object>(qb: QueryBuilder<T, any, any, any>, meta: EntityMetadata<T>, populateOrderBy: QueryOrderMap<T>[], parentPath: string, explicit: boolean, parentAlias?: string): QueryOrderMap<T>[];
142
148
  protected buildJoinedPropsOrderBy<T extends object>(qb: QueryBuilder<T, any, any, any>, meta: EntityMetadata<T>, populate: PopulateOptions<T>[], options?: Pick<FindOptions<any>, 'strategy' | 'orderBy' | 'populateOrderBy'>, parentPath?: string): QueryOrderMap<T>[];
@@ -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) {
@@ -493,6 +496,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
493
496
  if (meta.virtual) {
494
497
  return this.countVirtual(entityName, where, options);
495
498
  }
499
+ if (options.unionWhere?.length) {
500
+ where = await this.applyUnionWhere(meta, where, options);
501
+ }
496
502
  options = { populate: [], ...options };
497
503
  const populate = options.populate;
498
504
  const joinedProps = this.joinedProps(meta, populate, options);
@@ -704,6 +710,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
704
710
  /* v8 ignore next */
705
711
  where = { [meta.primaryKeys[0] ?? pks[0]]: where };
706
712
  }
713
+ if (!options.upsert && options.unionWhere?.length) {
714
+ where = await this.applyUnionWhere(meta, where, options, true);
715
+ }
707
716
  if (Utils.hasObjectKeys(data)) {
708
717
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', options.convertCustomTypes, options.loggerContext).withSchema(this.getSchemaName(meta, options));
709
718
  if (options.upsert) {
@@ -881,6 +890,9 @@ export class AbstractSqlDriver extends DatabaseDriver {
881
890
  if (Utils.isPrimaryKey(where) && pks.length === 1) {
882
891
  where = { [pks[0]]: where };
883
892
  }
893
+ if (options.unionWhere?.length) {
894
+ where = await this.applyUnionWhere(meta, where, options, true);
895
+ }
884
896
  const qb = this.createQueryBuilder(entityName, options.ctx, 'write', false, options.loggerContext)
885
897
  .delete(where)
886
898
  .withSchema(this.getSchemaName(meta, options));
@@ -1598,6 +1610,47 @@ export class AbstractSqlDriver extends DatabaseDriver {
1598
1610
  /* v8 ignore next */
1599
1611
  return { $and: [options.populateWhere, where] };
1600
1612
  }
1613
+ /**
1614
+ * Builds a UNION ALL (or UNION) subquery from `unionWhere` branches and merges it
1615
+ * into the main WHERE as `pk IN (branch_1 UNION ALL branch_2 ...)`.
1616
+ * Each branch is planned independently by the database, enabling per-table index usage.
1617
+ */
1618
+ async applyUnionWhere(meta, where, options, forDml = false) {
1619
+ const unionWhere = options.unionWhere;
1620
+ const strategy = options.unionWhereStrategy ?? 'union-all';
1621
+ const schema = this.getSchemaName(meta, options);
1622
+ const connectionType = this.resolveConnectionType({
1623
+ ctx: options.ctx,
1624
+ connectionType: options.connectionType,
1625
+ });
1626
+ const branchQbs = [];
1627
+ for (const branch of unionWhere) {
1628
+ const qb = this.createQueryBuilder(meta.class, options.ctx, connectionType, false, options.logging).withSchema(schema);
1629
+ const pkFields = meta.primaryKeys.map(pk => {
1630
+ const prop = meta.properties[pk];
1631
+ return `${qb.alias}.${prop.fieldNames[0]}`;
1632
+ });
1633
+ qb.select(pkFields).where(branch);
1634
+ if (options.em) {
1635
+ await qb.applyJoinedFilters(options.em, options.filters);
1636
+ }
1637
+ branchQbs.push(qb);
1638
+ }
1639
+ const [first, ...rest] = branchQbs;
1640
+ const unionQb = strategy === 'union' ? first.union(...rest) : first.unionAll(...rest);
1641
+ const pkHash = Utils.getPrimaryKeyHash(meta.primaryKeys);
1642
+ // MySQL does not allow referencing the target table in a subquery
1643
+ // for UPDATE/DELETE, so we wrap the union in a derived table.
1644
+ if (forDml) {
1645
+ const { sql, params } = unionQb.toQuery();
1646
+ return {
1647
+ $and: [where, { [pkHash]: { $in: raw(`select * from (${sql}) as __u`, params) } }],
1648
+ };
1649
+ }
1650
+ return {
1651
+ $and: [where, { [pkHash]: { $in: unionQb.toRaw() } }],
1652
+ };
1653
+ }
1601
1654
  buildOrderBy(qb, meta, populate, options) {
1602
1655
  const joinedProps = this.joinedProps(meta, populate, options);
1603
1656
  // `options._populateWhere` is a copy of the value provided by user with a fallback to the global config option
@@ -27,6 +27,7 @@ export declare abstract class AbstractSqlPlatform extends Platform {
27
27
  getSearchJsonPropertySQL(path: string, type: string, aliased: boolean): string | RawQueryFragment;
28
28
  getSearchJsonPropertyKey(path: string[], type: string, aliased: boolean, value?: unknown): string | RawQueryFragment;
29
29
  getJsonIndexDefinition(index: IndexDef): string[];
30
+ supportsUnionWhere(): boolean;
30
31
  supportsSchemas(): boolean;
31
32
  /** @inheritDoc */
32
33
  generateCustomOrder(escapedColumn: string, values: unknown[]): string;
@@ -80,6 +80,9 @@ export class AbstractSqlPlatform extends Platform {
80
80
  return `(json_extract(${root}, '$.${path.join('.')}'))`;
81
81
  });
82
82
  }
83
+ supportsUnionWhere() {
84
+ return true;
85
+ }
83
86
  supportsSchemas() {
84
87
  return false;
85
88
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mikro-orm/sql",
3
- "version": "7.0.0-dev.291",
3
+ "version": "7.0.0-dev.293",
4
4
  "description": "TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, PostgreSQL and SQLite databases as well as usage with vanilla JavaScript.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -56,6 +56,6 @@
56
56
  "@mikro-orm/core": "^6.6.8"
57
57
  },
58
58
  "peerDependencies": {
59
- "@mikro-orm/core": "7.0.0-dev.291"
59
+ "@mikro-orm/core": "7.0.0-dev.293"
60
60
  }
61
61
  }
@@ -220,6 +220,10 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
220
220
  params?: readonly unknown[];
221
221
  qb: NativeQueryBuilder;
222
222
  };
223
+ protected _unionQuery?: {
224
+ sql: string;
225
+ params: readonly unknown[];
226
+ };
223
227
  protected readonly platform: AbstractSqlPlatform;
224
228
  private tptJoinsApplied;
225
229
  private readonly autoJoinedPaths;
@@ -750,6 +754,38 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
750
754
  * You can provide the target entity name as the first parameter and use the second parameter to point to an existing property to infer its field name.
751
755
  */
752
756
  as<T>(targetEntity: EntityName<T>, alias: EntityKey<T>): NativeQueryBuilder;
757
+ /**
758
+ * Combines the current query with one or more other queries using `UNION ALL`.
759
+ * All queries must select the same columns. Returns a `QueryBuilder` that
760
+ * can be used with `$in`, passed to `qb.from()`, or converted via `.getQuery()`,
761
+ * `.getParams()`, `.toQuery()`, `.toRaw()`, etc.
762
+ *
763
+ * ```ts
764
+ * const qb1 = em.createQueryBuilder(Employee).select('id').where(condition1);
765
+ * const qb2 = em.createQueryBuilder(Employee).select('id').where(condition2);
766
+ * const qb3 = em.createQueryBuilder(Employee).select('id').where(condition3);
767
+ * const subquery = qb1.unionAll(qb2, qb3);
768
+ *
769
+ * const results = await em.find(Employee, { id: { $in: subquery } });
770
+ * ```
771
+ */
772
+ unionAll(...others: (QueryBuilder<any> | NativeQueryBuilder)[]): QueryBuilder<Entity>;
773
+ /**
774
+ * Combines the current query with one or more other queries using `UNION` (with deduplication).
775
+ * All queries must select the same columns. Returns a `QueryBuilder` that
776
+ * can be used with `$in`, passed to `qb.from()`, or converted via `.getQuery()`,
777
+ * `.getParams()`, `.toQuery()`, `.toRaw()`, etc.
778
+ *
779
+ * ```ts
780
+ * const qb1 = em.createQueryBuilder(Employee).select('id').where(condition1);
781
+ * const qb2 = em.createQueryBuilder(Employee).select('id').where(condition2);
782
+ * const subquery = qb1.union(qb2);
783
+ *
784
+ * const results = await em.find(Employee, { id: { $in: subquery } });
785
+ * ```
786
+ */
787
+ union(...others: (QueryBuilder<any> | NativeQueryBuilder)[]): QueryBuilder<Entity>;
788
+ private buildUnionQuery;
753
789
  clone(reset?: boolean | string[], preserve?: string[]): QueryBuilder<Entity, RootAlias, Hint, Context, RawAliases, Fields>;
754
790
  /**
755
791
  * Sets logger context for this query builder.
@@ -836,7 +872,7 @@ export declare class QueryBuilder<Entity extends object = AnyEntity, RootAlias e
836
872
  private wrapModifySubQuery;
837
873
  private getSchema;
838
874
  /** @internal */
839
- createAlias<U = unknown>(entityName: EntityName<U>, aliasName: string, subQuery?: NativeQueryBuilder): Alias<U>;
875
+ createAlias<U = unknown>(entityName: EntityName<U>, aliasName: string, subQuery?: NativeQueryBuilder | RawQueryFragment): Alias<U>;
840
876
  private createMainAlias;
841
877
  private fromSubQuery;
842
878
  private fromEntityName;
@@ -81,6 +81,7 @@ export class QueryBuilder {
81
81
  _tptAlias = {}; // maps entity className to alias for TPT parent tables
82
82
  _helper;
83
83
  _query;
84
+ _unionQuery;
84
85
  platform;
85
86
  tptJoinsApplied = false;
86
87
  autoJoinedPaths = [];
@@ -720,6 +721,16 @@ export class QueryBuilder {
720
721
  return this;
721
722
  }
722
723
  getNativeQuery(processVirtualEntity = true) {
724
+ if (this._unionQuery) {
725
+ if (!this._query?.qb) {
726
+ this._query = {};
727
+ const nqb = this.platform.createNativeQueryBuilder();
728
+ nqb.select('*');
729
+ nqb.from(raw(`(${this._unionQuery.sql})`, this._unionQuery.params));
730
+ this._query.qb = nqb;
731
+ }
732
+ return this._query.qb;
733
+ }
723
734
  if (this._query?.qb) {
724
735
  return this._query.qb;
725
736
  }
@@ -764,6 +775,9 @@ export class QueryBuilder {
764
775
  return raw(sql, params);
765
776
  }
766
777
  toQuery() {
778
+ if (this._unionQuery) {
779
+ return this._unionQuery;
780
+ }
767
781
  if (this._query?.sql) {
768
782
  return { sql: this._query.sql, params: this._query.params };
769
783
  }
@@ -1059,6 +1073,54 @@ export class QueryBuilder {
1059
1073
  Object.defineProperty(qb, '__as', { enumerable: false, value: finalAlias });
1060
1074
  return qb;
1061
1075
  }
1076
+ /**
1077
+ * Combines the current query with one or more other queries using `UNION ALL`.
1078
+ * All queries must select the same columns. Returns a `QueryBuilder` that
1079
+ * can be used with `$in`, passed to `qb.from()`, or converted via `.getQuery()`,
1080
+ * `.getParams()`, `.toQuery()`, `.toRaw()`, etc.
1081
+ *
1082
+ * ```ts
1083
+ * const qb1 = em.createQueryBuilder(Employee).select('id').where(condition1);
1084
+ * const qb2 = em.createQueryBuilder(Employee).select('id').where(condition2);
1085
+ * const qb3 = em.createQueryBuilder(Employee).select('id').where(condition3);
1086
+ * const subquery = qb1.unionAll(qb2, qb3);
1087
+ *
1088
+ * const results = await em.find(Employee, { id: { $in: subquery } });
1089
+ * ```
1090
+ */
1091
+ unionAll(...others) {
1092
+ return this.buildUnionQuery('union all', others);
1093
+ }
1094
+ /**
1095
+ * Combines the current query with one or more other queries using `UNION` (with deduplication).
1096
+ * All queries must select the same columns. Returns a `QueryBuilder` that
1097
+ * can be used with `$in`, passed to `qb.from()`, or converted via `.getQuery()`,
1098
+ * `.getParams()`, `.toQuery()`, `.toRaw()`, etc.
1099
+ *
1100
+ * ```ts
1101
+ * const qb1 = em.createQueryBuilder(Employee).select('id').where(condition1);
1102
+ * const qb2 = em.createQueryBuilder(Employee).select('id').where(condition2);
1103
+ * const subquery = qb1.union(qb2);
1104
+ *
1105
+ * const results = await em.find(Employee, { id: { $in: subquery } });
1106
+ * ```
1107
+ */
1108
+ union(...others) {
1109
+ return this.buildUnionQuery('union', others);
1110
+ }
1111
+ buildUnionQuery(separator, others) {
1112
+ const all = [this, ...others];
1113
+ const parts = [];
1114
+ const params = [];
1115
+ for (const qb of all) {
1116
+ const compiled = qb instanceof QueryBuilder ? qb.toQuery() : qb.compile();
1117
+ parts.push(`(${compiled.sql})`);
1118
+ params.push(...compiled.params);
1119
+ }
1120
+ const result = this.clone(true);
1121
+ result._unionQuery = { sql: parts.join(` ${separator} `), params };
1122
+ return result;
1123
+ }
1062
1124
  clone(reset, preserve) {
1063
1125
  const qb = new QueryBuilder(this.mainAlias.entityName, this.metadata, this.driver, this.context, this.mainAlias.aliasName, this.connectionType, this.em);
1064
1126
  reset = reset || [];
@@ -1066,7 +1128,7 @@ export class QueryBuilder {
1066
1128
  const properties = [
1067
1129
  'flags', '_populate', '_populateWhere', '_populateFilter', '__populateWhere', '_populateMap', '_joins', '_joinedProps', '_cond', '_data', '_orderBy',
1068
1130
  '_schema', '_indexHint', '_collation', '_cache', 'subQueries', 'lockMode', 'lockTables', '_groupBy', '_having', '_returning',
1069
- '_comments', '_hintComments', 'aliasCounter',
1131
+ '_comments', '_hintComments', 'aliasCounter', '_unionQuery',
1070
1132
  ];
1071
1133
  for (const prop of Object.keys(this)) {
1072
1134
  if (!preserve?.includes(prop) && (reset === true || reset.includes(prop) || ['_helper', '_query'].includes(prop))) {
@@ -1399,7 +1461,11 @@ export class QueryBuilder {
1399
1461
  const requiresAlias = this.finalized && (this._explicitAlias || this.helper.isTableNameAliasRequired(this.type));
1400
1462
  const alias = requiresAlias ? aliasName : undefined;
1401
1463
  const schema = this.getSchema(this.mainAlias);
1402
- const tableName = subQuery ? subQuery.as(aliasName) : this.helper.getTableName(entityName);
1464
+ const tableName = subQuery instanceof NativeQueryBuilder
1465
+ ? subQuery.as(aliasName)
1466
+ : subQuery
1467
+ ? raw(`(${subQuery.sql}) as ${this.platform.quoteIdentifier(aliasName)}`, subQuery.params)
1468
+ : this.helper.getTableName(entityName);
1403
1469
  const joinSchema = this._schema ?? this.em?.schema ?? schema;
1404
1470
  if (meta.virtual && processVirtualEntity) {
1405
1471
  qb.from(raw(this.fromVirtual(meta)), { indexHint: this._indexHint });
@@ -1946,9 +2012,9 @@ export class QueryBuilder {
1946
2012
  return this._mainAlias;
1947
2013
  }
1948
2014
  fromSubQuery(target, aliasName) {
1949
- const subQuery = target.getNativeQuery();
1950
2015
  const { entityName } = target.mainAlias;
1951
2016
  aliasName ??= this.getNextAlias(entityName);
2017
+ const subQuery = target._unionQuery ? target.toRaw() : target.getNativeQuery();
1952
2018
  this.createMainAlias(entityName, aliasName, subQuery);
1953
2019
  }
1954
2020
  fromEntityName(entityName, aliasName) {
@@ -1,8 +1,8 @@
1
- import { type Dictionary, type EntityData, type EntityKey, type EntityMetadata, type EntityName, type EntityProperty, type FilterQuery, type FlatQueryOrderMap, type FormulaTable, LockMode, type QueryOrderMap, Raw, type RawQueryFragmentSymbol } from '@mikro-orm/core';
1
+ import { type Dictionary, type EntityData, type EntityKey, type EntityMetadata, type EntityName, type EntityProperty, type FilterQuery, type FlatQueryOrderMap, type FormulaTable, LockMode, type QueryOrderMap, Raw, type RawQueryFragment, type RawQueryFragmentSymbol } from '@mikro-orm/core';
2
2
  import { JoinType, QueryType } from './enums.js';
3
3
  import type { InternalField, JoinOptions } from '../typings.js';
4
4
  import type { AbstractSqlDriver } from '../AbstractSqlDriver.js';
5
- import { NativeQueryBuilder } from './NativeQueryBuilder.js';
5
+ import type { NativeQueryBuilder } from './NativeQueryBuilder.js';
6
6
  /**
7
7
  * @internal
8
8
  */
@@ -71,7 +71,7 @@ export interface Alias<T> {
71
71
  aliasName: string;
72
72
  entityName: EntityName<T>;
73
73
  meta: EntityMetadata<T>;
74
- subQuery?: NativeQueryBuilder;
74
+ subQuery?: NativeQueryBuilder | RawQueryFragment;
75
75
  }
76
76
  export interface OnConflictClause<T> {
77
77
  fields: string[] | Raw;
@@ -1,6 +1,5 @@
1
1
  import { ALIAS_REPLACEMENT, ALIAS_REPLACEMENT_RE, ArrayType, inspect, isRaw, LockMode, OptimisticLockError, QueryOperator, QueryOrderNumeric, raw, Raw, QueryHelper, ReferenceKind, Utils, ValidationError, } from '@mikro-orm/core';
2
2
  import { JoinType, QueryType } from './enums.js';
3
- import { NativeQueryBuilder } from './NativeQueryBuilder.js';
4
3
  /**
5
4
  * @internal
6
5
  */
@@ -521,8 +520,8 @@ export class QueryBuilderHelper {
521
520
  else if (['$in', '$nin'].includes(op) && Array.isArray(value[op]) && value[op].length === 0) {
522
521
  parts.push(`1 = ${op === '$in' ? 0 : 1}`);
523
522
  }
524
- else if (value[op] instanceof Raw || value[op] instanceof NativeQueryBuilder) {
525
- const query = value[op] instanceof NativeQueryBuilder ? value[op].toRaw() : value[op];
523
+ else if (value[op] instanceof Raw || (typeof value[op]?.toRaw === 'function')) {
524
+ const query = value[op] instanceof Raw ? value[op] : value[op].toRaw();
526
525
  const mappedKey = this.mapper(key, type, query, null);
527
526
  let sql = query.sql;
528
527
  if (['$in', '$nin'].includes(op)) {
@@ -22,7 +22,7 @@ export class ScalarCriteriaNode extends CriteriaNode {
22
22
  }
23
23
  }
24
24
  if (this.payload instanceof QueryBuilder) {
25
- return this.payload.getNativeQuery().toRaw();
25
+ return this.payload.toRaw();
26
26
  }
27
27
  if (this.payload && typeof this.payload === 'object') {
28
28
  const keys = Object.keys(this.payload).filter(key => ARRAY_OPERATORS.includes(key) && Array.isArray(this.payload[key]));