metal-orm 1.0.12 → 1.0.14

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.
package/README.md CHANGED
@@ -1,11 +1,9 @@
1
- # MetalORM ⚙️
1
+ # MetalORM ⚙️ - Type-safe SQL, layered ORM, decorator-based entities – all on the same core.
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/metal-orm.svg)](https://www.npmjs.com/package/metal-orm)
4
4
  [![license](https://img.shields.io/npm/l/metal-orm.svg)](https://github.com/celsowm/metal-orm/blob/main/LICENSE)
5
5
  [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%23007ACC.svg)](https://www.typescriptlang.org/)
6
6
 
7
- **Type-safe SQL, layered ORM, decorator-based entities – all on the same core.**
8
-
9
7
  MetalORM is a TypeScript-first, AST-driven SQL toolkit you can dial up or down depending on how “ORM-y” you want to be:
10
8
 
11
9
  - **Level 1 – Query builder & hydration 🧩**
@@ -22,10 +20,10 @@ Use only the layer you need in each part of your codebase.
22
20
  <a id="table-of-contents"></a>
23
21
  ## Table of Contents 🧭
24
22
 
25
- - [Documentation](#documentation)
26
- - [Features](#features)
27
- - [Installation](#installation)
28
- - [Quick start - three levels](#quick-start)
23
+ - [Documentation](#documentation)
24
+ - [Features](#features)
25
+ - [Installation](#installation)
26
+ - [Quick start - three levels](#quick-start)
29
27
  - [Level 1 – Query builder & hydration](#level-1)
30
28
  - [Level 2 – Entities + Unit of Work](#level-2)
31
29
  - [Level 3 – Decorator entities](#level-3)
@@ -41,18 +39,18 @@ Use only the layer you need in each part of your codebase.
41
39
 
42
40
  Full docs live in the `docs/` folder:
43
41
 
44
- - [Introduction](https://github.com/celsowm/metal-orm/blob/main/docs/index.md)
45
- - [Getting Started](https://github.com/celsowm/metal-orm/blob/main/docs/getting-started.md)
46
- - [Level 3 Backend Tutorial](https://github.com/celsowm/metal-orm/blob/main/docs/level-3-backend-tutorial.md)
47
- - [Schema Definition](https://github.com/celsowm/metal-orm/blob/main/docs/schema-definition.md)
48
- - [Query Builder](https://github.com/celsowm/metal-orm/blob/main/docs/query-builder.md)
49
- - [DML Operations](https://github.com/celsowm/metal-orm/blob/main/docs/dml-operations.md)
50
- - [Hydration & Entities](https://github.com/celsowm/metal-orm/blob/main/docs/hydration.md)
51
- - [Runtime & Unit of Work](https://github.com/celsowm/metal-orm/blob/main/docs/runtime.md)
52
- - [Advanced Features](https://github.com/celsowm/metal-orm/blob/main/docs/advanced-features.md)
53
- - [Multi-Dialect Support](https://github.com/celsowm/metal-orm/blob/main/docs/multi-dialect-support.md)
54
- - [Schema Generation (DDL)](https://github.com/celsowm/metal-orm/blob/main/docs/schema-generation.md)
55
- - [API Reference](https://github.com/celsowm/metal-orm/blob/main/docs/api-reference.md)
42
+ - [Introduction](https://github.com/celsowm/metal-orm/blob/main/docs/index.md)
43
+ - [Getting Started](https://github.com/celsowm/metal-orm/blob/main/docs/getting-started.md)
44
+ - [Level 3 Backend Tutorial](https://github.com/celsowm/metal-orm/blob/main/docs/level-3-backend-tutorial.md)
45
+ - [Schema Definition](https://github.com/celsowm/metal-orm/blob/main/docs/schema-definition.md)
46
+ - [Query Builder](https://github.com/celsowm/metal-orm/blob/main/docs/query-builder.md)
47
+ - [DML Operations](https://github.com/celsowm/metal-orm/blob/main/docs/dml-operations.md)
48
+ - [Hydration & Entities](https://github.com/celsowm/metal-orm/blob/main/docs/hydration.md)
49
+ - [Runtime & Unit of Work](https://github.com/celsowm/metal-orm/blob/main/docs/runtime.md)
50
+ - [Advanced Features](https://github.com/celsowm/metal-orm/blob/main/docs/advanced-features.md)
51
+ - [Multi-Dialect Support](https://github.com/celsowm/metal-orm/blob/main/docs/multi-dialect-support.md)
52
+ - [Schema Generation (DDL)](https://github.com/celsowm/metal-orm/blob/main/docs/schema-generation.md)
53
+ - [API Reference](https://github.com/celsowm/metal-orm/blob/main/docs/api-reference.md)
56
54
 
57
55
  ---
58
56
 
@@ -65,6 +63,7 @@ Full docs live in the `docs/` folder:
65
63
  - **Fluent query builder** over a real SQL AST
66
64
  (`SelectQueryBuilder`, `InsertQueryBuilder`, `UpdateQueryBuilder`, `DeleteQueryBuilder`).
67
65
  - **Advanced SQL**: CTEs, aggregates, window functions, subqueries, JSON, CASE, EXISTS.
66
+ - **Set operations**: `union`, `unionAll`, `intersect`, `except` across all dialects (ORDER/LIMIT apply to the combined result; hydration is disabled for compound queries so rows are returned as-is without collapsing duplicates).
68
67
  - **Expression builders**: `eq`, `and`, `or`, `between`, `inList`, `exists`, `jsonPath`, `caseWhen`, window functions like `rowNumber`, `rank`, `lag`, `lead`, etc., all backed by typed AST nodes.
69
68
  - **Relation-aware hydration**: turn flat rows into nested objects (`user.posts`, `user.roles`, etc.) using a hydration plan derived from the AST metadata.
70
69
  - **Multi-dialect**: compile once, run on MySQL/MariaDB, PostgreSQL, SQLite, or SQL Server via pluggable dialects.
@@ -88,6 +87,7 @@ On top of the query builder, MetalORM ships a focused runtime:
88
87
  - **Relation change processor** that knows how to deal with has-many and many-to-many pivot tables.
89
88
  - **Interceptors**: `beforeFlush` / `afterFlush` hooks for cross-cutting concerns (auditing, multi-tenant filters, soft delete filters, etc.).
90
89
  - **Domain events**: `addDomainEvent` and a DomainEventBus integrated into `saveChanges()`, aligned with domain events from [Domain-driven design](https://en.wikipedia.org/wiki/Domain-driven_design).
90
+ - **JSON-safe entities**: relation wrappers hide internal references and implement `toJSON`, so `JSON.stringify` of hydrated entities works without circular reference errors.
91
91
 
92
92
  Use this layer where:
93
93
 
@@ -494,4 +494,4 @@ See the contributing guide for details.
494
494
  <a id="license"></a>
495
495
  ## License 📄
496
496
 
497
- MetalORM is MIT licensed.
497
+ MetalORM is MIT licensed.
@@ -500,6 +500,44 @@ var SelectQueryState = class _SelectQueryState {
500
500
  ctes: [...this.ast.ctes ?? [], cte]
501
501
  });
502
502
  }
503
+ /**
504
+ * Adds a set operation (UNION/INTERSECT/EXCEPT) to the query
505
+ * @param op - Set operation node to add
506
+ * @returns New SelectQueryState with set operation
507
+ */
508
+ withSetOperation(op) {
509
+ return this.clone({
510
+ ...this.ast,
511
+ setOps: [...this.ast.setOps ?? [], op]
512
+ });
513
+ }
514
+ };
515
+
516
+ // src/core/ast/join-node.ts
517
+ var createJoinNode = (kind, tableName, condition, relationName) => ({
518
+ type: "Join",
519
+ kind,
520
+ table: { type: "Table", name: tableName },
521
+ condition,
522
+ relationName
523
+ });
524
+
525
+ // src/core/sql/sql.ts
526
+ var JOIN_KINDS = {
527
+ /** INNER JOIN type */
528
+ INNER: "INNER",
529
+ /** LEFT JOIN type */
530
+ LEFT: "LEFT",
531
+ /** RIGHT JOIN type */
532
+ RIGHT: "RIGHT",
533
+ /** CROSS JOIN type */
534
+ CROSS: "CROSS"
535
+ };
536
+ var ORDER_DIRECTIONS = {
537
+ /** Ascending order */
538
+ ASC: "ASC",
539
+ /** Descending order */
540
+ DESC: "DESC"
503
541
  };
504
542
 
505
543
  // src/query-builder/hydration-manager.ts
@@ -551,8 +589,26 @@ var HydrationManager = class _HydrationManager {
551
589
  * @returns AST with hydration metadata
552
590
  */
553
591
  applyToAst(ast) {
592
+ if (ast.setOps && ast.setOps.length > 0) {
593
+ return ast;
594
+ }
554
595
  const plan = this.planner.getPlan();
555
596
  if (!plan) return ast;
597
+ const needsPaginationGuard = this.requiresParentPagination(ast, plan);
598
+ const rewritten = needsPaginationGuard ? this.wrapForParentPagination(ast, plan) : ast;
599
+ return this.attachHydrationMeta(rewritten, plan);
600
+ }
601
+ /**
602
+ * Gets the current hydration plan
603
+ * @returns Hydration plan or undefined if none exists
604
+ */
605
+ getPlan() {
606
+ return this.planner.getPlan();
607
+ }
608
+ /**
609
+ * Attaches hydration metadata to a query AST node.
610
+ */
611
+ attachHydrationMeta(ast, plan) {
556
612
  return {
557
613
  ...ast,
558
614
  meta: {
@@ -562,11 +618,154 @@ var HydrationManager = class _HydrationManager {
562
618
  };
563
619
  }
564
620
  /**
565
- * Gets the current hydration plan
566
- * @returns Hydration plan or undefined if none exists
621
+ * Determines whether the query needs pagination rewriting to keep LIMIT/OFFSET
622
+ * applied to parent rows when eager-loading multiplicative relations.
567
623
  */
568
- getPlan() {
569
- return this.planner.getPlan();
624
+ requiresParentPagination(ast, plan) {
625
+ const hasPagination = ast.limit !== void 0 || ast.offset !== void 0;
626
+ return hasPagination && this.hasMultiplyingRelations(plan);
627
+ }
628
+ hasMultiplyingRelations(plan) {
629
+ return plan.relations.some(
630
+ (rel) => rel.type === RelationKinds.HasMany || rel.type === RelationKinds.BelongsToMany
631
+ );
632
+ }
633
+ /**
634
+ * Rewrites the query using CTEs so LIMIT/OFFSET target distinct parent rows
635
+ * instead of the joined result set.
636
+ *
637
+ * The strategy:
638
+ * - Hoist the original query (minus limit/offset) into a base CTE.
639
+ * - Select distinct parent ids from that base CTE with the original ordering and pagination.
640
+ * - Join the base CTE against the paged ids to retrieve the joined rows for just that page.
641
+ */
642
+ wrapForParentPagination(ast, plan) {
643
+ const projectionNames = this.getProjectionNames(ast.columns);
644
+ if (!projectionNames) {
645
+ return ast;
646
+ }
647
+ const projectionAliases = this.buildProjectionAliasMap(ast.columns);
648
+ const projectionSet = new Set(projectionNames);
649
+ const rootPkAlias = projectionAliases.get(`${plan.rootTable}.${plan.rootPrimaryKey}`) ?? plan.rootPrimaryKey;
650
+ const baseCteName = this.nextCteName(ast.ctes, "__metal_pagination_base");
651
+ const baseQuery = {
652
+ ...ast,
653
+ ctes: void 0,
654
+ limit: void 0,
655
+ offset: void 0,
656
+ orderBy: void 0,
657
+ meta: void 0
658
+ };
659
+ const baseCte = {
660
+ type: "CommonTableExpression",
661
+ name: baseCteName,
662
+ query: baseQuery,
663
+ recursive: false
664
+ };
665
+ const orderBy = this.mapOrderBy(ast.orderBy, plan, projectionAliases, baseCteName, projectionSet);
666
+ if (orderBy === null) {
667
+ return ast;
668
+ }
669
+ const pageCteName = this.nextCteName([...ast.ctes ?? [], baseCte], "__metal_pagination_page");
670
+ const pagingColumns = this.buildPagingColumns(rootPkAlias, orderBy, baseCteName);
671
+ const pageCte = {
672
+ type: "CommonTableExpression",
673
+ name: pageCteName,
674
+ query: {
675
+ type: "SelectQuery",
676
+ from: { type: "Table", name: baseCteName },
677
+ columns: pagingColumns,
678
+ joins: [],
679
+ distinct: [{ type: "Column", table: baseCteName, name: rootPkAlias }],
680
+ orderBy,
681
+ limit: ast.limit,
682
+ offset: ast.offset
683
+ },
684
+ recursive: false
685
+ };
686
+ const joinCondition = eq(
687
+ { type: "Column", table: baseCteName, name: rootPkAlias },
688
+ { type: "Column", table: pageCteName, name: rootPkAlias }
689
+ );
690
+ const outerColumns = projectionNames.map((name) => ({
691
+ type: "Column",
692
+ table: baseCteName,
693
+ name,
694
+ alias: name
695
+ }));
696
+ return {
697
+ type: "SelectQuery",
698
+ from: { type: "Table", name: baseCteName },
699
+ columns: outerColumns,
700
+ joins: [createJoinNode(JOIN_KINDS.INNER, pageCteName, joinCondition)],
701
+ orderBy,
702
+ ctes: [...ast.ctes ?? [], baseCte, pageCte]
703
+ };
704
+ }
705
+ nextCteName(existing, baseName) {
706
+ const names = new Set((existing ?? []).map((cte) => cte.name));
707
+ let candidate = baseName;
708
+ let suffix = 1;
709
+ while (names.has(candidate)) {
710
+ suffix += 1;
711
+ candidate = `${baseName}_${suffix}`;
712
+ }
713
+ return candidate;
714
+ }
715
+ getProjectionNames(columns) {
716
+ const names = [];
717
+ for (const col of columns) {
718
+ const alias = col.alias ?? col.name;
719
+ if (!alias) return void 0;
720
+ names.push(alias);
721
+ }
722
+ return names;
723
+ }
724
+ buildProjectionAliasMap(columns) {
725
+ const map = /* @__PURE__ */ new Map();
726
+ for (const col of columns) {
727
+ if (col.type !== "Column") continue;
728
+ const node = col;
729
+ const key = `${node.table}.${node.name}`;
730
+ map.set(key, node.alias ?? node.name);
731
+ }
732
+ return map;
733
+ }
734
+ mapOrderBy(orderBy, plan, projectionAliases, baseAlias, availableColumns) {
735
+ if (!orderBy || orderBy.length === 0) {
736
+ return void 0;
737
+ }
738
+ const mapped = [];
739
+ for (const ob of orderBy) {
740
+ if (ob.column.table !== plan.rootTable) {
741
+ return null;
742
+ }
743
+ const alias = projectionAliases.get(`${ob.column.table}.${ob.column.name}`) ?? ob.column.name;
744
+ if (!availableColumns.has(alias)) {
745
+ return null;
746
+ }
747
+ mapped.push({
748
+ type: "OrderBy",
749
+ column: { type: "Column", table: baseAlias, name: alias },
750
+ direction: ob.direction
751
+ });
752
+ }
753
+ return mapped;
754
+ }
755
+ buildPagingColumns(primaryKey, orderBy, tableAlias) {
756
+ const columns = [{ type: "Column", table: tableAlias, name: primaryKey, alias: primaryKey }];
757
+ if (!orderBy) return columns;
758
+ for (const ob of orderBy) {
759
+ if (!columns.some((col) => col.name === ob.column.name)) {
760
+ columns.push({
761
+ type: "Column",
762
+ table: tableAlias,
763
+ name: ob.column.name,
764
+ alias: ob.column.name
765
+ });
766
+ }
767
+ }
768
+ return columns;
570
769
  }
571
770
  };
572
771
 
@@ -820,6 +1019,20 @@ var QueryAstService = class {
820
1019
  };
821
1020
  return this.state.withCte(cte);
822
1021
  }
1022
+ /**
1023
+ * Adds a set operation (UNION/UNION ALL/INTERSECT/EXCEPT) to the query
1024
+ * @param operator - Set operator
1025
+ * @param query - Right-hand side query
1026
+ * @returns Updated query state with set operation
1027
+ */
1028
+ withSetOperation(operator, query) {
1029
+ const op = {
1030
+ type: "SetOperation",
1031
+ operator,
1032
+ query
1033
+ };
1034
+ return this.state.withSetOperation(op);
1035
+ }
823
1036
  /**
824
1037
  * Selects a subquery as a column
825
1038
  * @param alias - Alias for the subquery
@@ -972,15 +1185,6 @@ var RelationProjectionHelper = class {
972
1185
  }
973
1186
  };
974
1187
 
975
- // src/core/ast/join-node.ts
976
- var createJoinNode = (kind, tableName, condition, relationName) => ({
977
- type: "Join",
978
- kind,
979
- table: { type: "Table", name: tableName },
980
- condition,
981
- relationName
982
- });
983
-
984
1188
  // src/query-builder/relation-conditions.ts
985
1189
  var assertNever = (value) => {
986
1190
  throw new Error(`Unhandled relation type: ${JSON.stringify(value)}`);
@@ -1036,24 +1240,6 @@ var buildRelationCorrelation = (root, relation) => {
1036
1240
  return baseRelationCondition(root, relation);
1037
1241
  };
1038
1242
 
1039
- // src/core/sql/sql.ts
1040
- var JOIN_KINDS = {
1041
- /** INNER JOIN type */
1042
- INNER: "INNER",
1043
- /** LEFT JOIN type */
1044
- LEFT: "LEFT",
1045
- /** RIGHT JOIN type */
1046
- RIGHT: "RIGHT",
1047
- /** CROSS JOIN type */
1048
- CROSS: "CROSS"
1049
- };
1050
- var ORDER_DIRECTIONS = {
1051
- /** Ascending order */
1052
- ASC: "ASC",
1053
- /** Descending order */
1054
- DESC: "DESC"
1055
- };
1056
-
1057
1243
  // src/query-builder/relation-service.ts
1058
1244
  var RelationService = class {
1059
1245
  /**
@@ -1499,6 +1685,16 @@ var hasEntityMeta = (entity) => {
1499
1685
 
1500
1686
  // src/orm/relations/has-many.ts
1501
1687
  var toKey2 = (value) => value === null || value === void 0 ? "" : String(value);
1688
+ var hideInternal = (obj, keys) => {
1689
+ for (const key of keys) {
1690
+ Object.defineProperty(obj, key, {
1691
+ value: obj[key],
1692
+ writable: false,
1693
+ configurable: false,
1694
+ enumerable: false
1695
+ });
1696
+ }
1697
+ };
1502
1698
  var DefaultHasManyCollection = class {
1503
1699
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, localKey) {
1504
1700
  this.ctx = ctx;
@@ -1514,6 +1710,7 @@ var DefaultHasManyCollection = class {
1514
1710
  this.items = [];
1515
1711
  this.added = /* @__PURE__ */ new Set();
1516
1712
  this.removed = /* @__PURE__ */ new Set();
1713
+ hideInternal(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "localKey"]);
1517
1714
  this.hydrateFromCache();
1518
1715
  }
1519
1716
  async load() {
@@ -1589,10 +1786,23 @@ var DefaultHasManyCollection = class {
1589
1786
  this.items = rows.map((row) => this.createEntity(row));
1590
1787
  this.loaded = true;
1591
1788
  }
1789
+ toJSON() {
1790
+ return this.items;
1791
+ }
1592
1792
  };
1593
1793
 
1594
1794
  // src/orm/relations/belongs-to.ts
1595
1795
  var toKey3 = (value) => value === null || value === void 0 ? "" : String(value);
1796
+ var hideInternal2 = (obj, keys) => {
1797
+ for (const key of keys) {
1798
+ Object.defineProperty(obj, key, {
1799
+ value: obj[key],
1800
+ writable: false,
1801
+ configurable: false,
1802
+ enumerable: false
1803
+ });
1804
+ }
1805
+ };
1596
1806
  var DefaultBelongsToReference = class {
1597
1807
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, targetKey) {
1598
1808
  this.ctx = ctx;
@@ -1606,6 +1816,7 @@ var DefaultBelongsToReference = class {
1606
1816
  this.targetKey = targetKey;
1607
1817
  this.loaded = false;
1608
1818
  this.current = null;
1819
+ hideInternal2(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "targetKey"]);
1609
1820
  this.populateFromHydrationCache();
1610
1821
  }
1611
1822
  async load() {
@@ -1666,10 +1877,23 @@ var DefaultBelongsToReference = class {
1666
1877
  this.current = this.createEntity(row);
1667
1878
  this.loaded = true;
1668
1879
  }
1880
+ toJSON() {
1881
+ return this.current;
1882
+ }
1669
1883
  };
1670
1884
 
1671
1885
  // src/orm/relations/many-to-many.ts
1672
1886
  var toKey4 = (value) => value === null || value === void 0 ? "" : String(value);
1887
+ var hideInternal3 = (obj, keys) => {
1888
+ for (const key of keys) {
1889
+ Object.defineProperty(obj, key, {
1890
+ value: obj[key],
1891
+ writable: false,
1892
+ configurable: false,
1893
+ enumerable: false
1894
+ });
1895
+ }
1896
+ };
1673
1897
  var DefaultManyToManyCollection = class {
1674
1898
  constructor(ctx, meta, root, relationName, relation, rootTable, loader, createEntity, localKey) {
1675
1899
  this.ctx = ctx;
@@ -1683,6 +1907,7 @@ var DefaultManyToManyCollection = class {
1683
1907
  this.localKey = localKey;
1684
1908
  this.loaded = false;
1685
1909
  this.items = [];
1910
+ hideInternal3(this, ["ctx", "meta", "root", "relationName", "relation", "rootTable", "loader", "createEntity", "localKey"]);
1686
1911
  this.hydrateFromCache();
1687
1912
  }
1688
1913
  async load() {
@@ -1788,6 +2013,9 @@ var DefaultManyToManyCollection = class {
1788
2013
  });
1789
2014
  this.loaded = true;
1790
2015
  }
2016
+ toJSON() {
2017
+ return this.items;
2018
+ }
1791
2019
  };
1792
2020
 
1793
2021
  // src/orm/lazy-batch.ts
@@ -2148,9 +2376,15 @@ var flattenResults = (results) => {
2148
2376
  return rows;
2149
2377
  };
2150
2378
  async function executeHydrated(ctx, qb) {
2151
- const compiled = ctx.dialect.compileSelect(qb.getAST());
2379
+ const ast = qb.getAST();
2380
+ const compiled = ctx.dialect.compileSelect(ast);
2152
2381
  const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
2153
2382
  const rows = flattenResults(executed);
2383
+ if (ast.setOps && ast.setOps.length > 0) {
2384
+ return rows.map(
2385
+ (row) => createEntityProxy(ctx, qb.getTable(), row, qb.getLazyRelations())
2386
+ );
2387
+ }
2154
2388
  const hydrated = hydrateRows(rows, qb.getHydrationPlan());
2155
2389
  return hydrated.map(
2156
2390
  (row) => createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
@@ -2197,6 +2431,10 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
2197
2431
  const joinNode = createJoinNode(kind, table.name, condition);
2198
2432
  return this.applyAst(context, (service) => service.withJoin(joinNode));
2199
2433
  }
2434
+ applySetOperation(operator, query) {
2435
+ const subAst = this.resolveQueryNode(query);
2436
+ return this.applyAst(this.context, (service) => service.withSetOperation(operator, subAst));
2437
+ }
2200
2438
  /**
2201
2439
  * Selects specific columns for the query
2202
2440
  * @param columns - Record of column definitions, function nodes, case expressions, or window functions
@@ -2385,6 +2623,38 @@ var SelectQueryBuilder = class _SelectQueryBuilder {
2385
2623
  const nextContext = this.applyAst(this.context, (service) => service.withOffset(n));
2386
2624
  return this.clone(nextContext);
2387
2625
  }
2626
+ /**
2627
+ * Combines this query with another using UNION
2628
+ * @param query - Query to union with
2629
+ * @returns New query builder instance with the set operation
2630
+ */
2631
+ union(query) {
2632
+ return this.clone(this.applySetOperation("UNION", query));
2633
+ }
2634
+ /**
2635
+ * Combines this query with another using UNION ALL
2636
+ * @param query - Query to union with
2637
+ * @returns New query builder instance with the set operation
2638
+ */
2639
+ unionAll(query) {
2640
+ return this.clone(this.applySetOperation("UNION ALL", query));
2641
+ }
2642
+ /**
2643
+ * Combines this query with another using INTERSECT
2644
+ * @param query - Query to intersect with
2645
+ * @returns New query builder instance with the set operation
2646
+ */
2647
+ intersect(query) {
2648
+ return this.clone(this.applySetOperation("INTERSECT", query));
2649
+ }
2650
+ /**
2651
+ * Combines this query with another using EXCEPT
2652
+ * @param query - Query to subtract
2653
+ * @returns New query builder instance with the set operation
2654
+ */
2655
+ except(query) {
2656
+ return this.clone(this.applySetOperation("EXCEPT", query));
2657
+ }
2388
2658
  /**
2389
2659
  * Adds a WHERE EXISTS condition to the query
2390
2660
  * @param subquery - Subquery to check for existence