metal-orm 1.0.11 → 1.0.13

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 (54) hide show
  1. package/README.md +21 -18
  2. package/dist/decorators/index.cjs +317 -34
  3. package/dist/decorators/index.cjs.map +1 -1
  4. package/dist/decorators/index.d.cts +1 -1
  5. package/dist/decorators/index.d.ts +1 -1
  6. package/dist/decorators/index.js +317 -34
  7. package/dist/decorators/index.js.map +1 -1
  8. package/dist/index.cjs +1965 -267
  9. package/dist/index.cjs.map +1 -1
  10. package/dist/index.d.cts +273 -23
  11. package/dist/index.d.ts +273 -23
  12. package/dist/index.js +1947 -267
  13. package/dist/index.js.map +1 -1
  14. package/dist/{select-654m4qy8.d.cts → select-CCp1oz9p.d.cts} +254 -4
  15. package/dist/{select-654m4qy8.d.ts → select-CCp1oz9p.d.ts} +254 -4
  16. package/package.json +3 -2
  17. package/src/core/ast/query.ts +40 -22
  18. package/src/core/ddl/dialects/base-schema-dialect.ts +48 -0
  19. package/src/core/ddl/dialects/index.ts +5 -0
  20. package/src/core/ddl/dialects/mssql-schema-dialect.ts +97 -0
  21. package/src/core/ddl/dialects/mysql-schema-dialect.ts +109 -0
  22. package/src/core/ddl/dialects/postgres-schema-dialect.ts +99 -0
  23. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +103 -0
  24. package/src/core/ddl/introspect/mssql.ts +149 -0
  25. package/src/core/ddl/introspect/mysql.ts +99 -0
  26. package/src/core/ddl/introspect/postgres.ts +154 -0
  27. package/src/core/ddl/introspect/sqlite.ts +66 -0
  28. package/src/core/ddl/introspect/types.ts +19 -0
  29. package/src/core/ddl/introspect/utils.ts +27 -0
  30. package/src/core/ddl/schema-diff.ts +179 -0
  31. package/src/core/ddl/schema-generator.ts +229 -0
  32. package/src/core/ddl/schema-introspect.ts +32 -0
  33. package/src/core/ddl/schema-types.ts +39 -0
  34. package/src/core/dialect/abstract.ts +122 -37
  35. package/src/core/dialect/base/sql-dialect.ts +204 -0
  36. package/src/core/dialect/mssql/index.ts +125 -80
  37. package/src/core/dialect/mysql/index.ts +18 -112
  38. package/src/core/dialect/postgres/index.ts +29 -126
  39. package/src/core/dialect/sqlite/index.ts +28 -129
  40. package/src/index.ts +4 -0
  41. package/src/orm/execute.ts +25 -16
  42. package/src/orm/orm-context.ts +60 -55
  43. package/src/orm/query-logger.ts +38 -0
  44. package/src/orm/relations/belongs-to.ts +42 -26
  45. package/src/orm/relations/has-many.ts +41 -25
  46. package/src/orm/relations/many-to-many.ts +43 -27
  47. package/src/orm/unit-of-work.ts +60 -23
  48. package/src/query-builder/hydration-manager.ts +229 -25
  49. package/src/query-builder/query-ast-service.ts +27 -12
  50. package/src/query-builder/select-query-state.ts +24 -12
  51. package/src/query-builder/select.ts +58 -14
  52. package/src/schema/column.ts +206 -27
  53. package/src/schema/table.ts +89 -32
  54. package/src/schema/types.ts +8 -5
@@ -1,11 +1,11 @@
1
- import { eq } from '../core/ast/expression.js';
2
- import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
1
+ import { ColumnNode, eq } from '../core/ast/expression.js';
2
+ import type { Dialect, CompiledQuery } from '../core/dialect/abstract.js';
3
3
  import { InsertQueryBuilder } from '../query-builder/insert.js';
4
4
  import { UpdateQueryBuilder } from '../query-builder/update.js';
5
5
  import { DeleteQueryBuilder } from '../query-builder/delete.js';
6
6
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
7
7
  import type { TableDef, TableHooks } from '../schema/table.js';
8
- import type { DbExecutor } from './db-executor.js';
8
+ import type { DbExecutor, QueryResult } from './db-executor.js';
9
9
  import { IdentityMap } from './identity-map.js';
10
10
  import { EntityStatus } from './runtime-types.js';
11
11
  import type { TrackedEntity } from './runtime-types.js';
@@ -120,10 +120,14 @@ export class UnitOfWork {
120
120
  private async flushInsert(tracked: TrackedEntity): Promise<void> {
121
121
  await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
122
122
 
123
- const payload = this.extractColumns(tracked.table, tracked.entity);
124
- const builder = new InsertQueryBuilder(tracked.table).values(payload);
125
- const compiled = builder.compile(this.dialect);
126
- await this.executeCompiled(compiled);
123
+ const payload = this.extractColumns(tracked.table, tracked.entity);
124
+ let builder = new InsertQueryBuilder(tracked.table).values(payload);
125
+ if (this.dialect.supportsReturning()) {
126
+ builder = builder.returning(...this.getReturningColumns(tracked.table));
127
+ }
128
+ const compiled = builder.compile(this.dialect);
129
+ const results = await this.executeCompiled(compiled);
130
+ this.applyReturningResults(tracked, results);
127
131
 
128
132
  tracked.status = EntityStatus.Managed;
129
133
  tracked.original = this.createSnapshot(tracked.table, tracked.entity);
@@ -146,12 +150,17 @@ export class UnitOfWork {
146
150
  const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
147
151
  if (!pkColumn) return;
148
152
 
149
- const builder = new UpdateQueryBuilder(tracked.table)
150
- .set(changes)
151
- .where(eq(pkColumn, tracked.pk));
152
-
153
- const compiled = builder.compile(this.dialect);
154
- await this.executeCompiled(compiled);
153
+ let builder = new UpdateQueryBuilder(tracked.table)
154
+ .set(changes)
155
+ .where(eq(pkColumn, tracked.pk));
156
+
157
+ if (this.dialect.supportsReturning()) {
158
+ builder = builder.returning(...this.getReturningColumns(tracked.table));
159
+ }
160
+
161
+ const compiled = builder.compile(this.dialect);
162
+ const results = await this.executeCompiled(compiled);
163
+ this.applyReturningResults(tracked, results);
155
164
 
156
165
  tracked.status = EntityStatus.Managed;
157
166
  tracked.original = this.createSnapshot(tracked.table, tracked.entity);
@@ -206,16 +215,44 @@ export class UnitOfWork {
206
215
  return payload;
207
216
  }
208
217
 
209
- private async executeCompiled(compiled: CompiledQuery): Promise<void> {
210
- await this.executor.executeSql(compiled.sql, compiled.params);
211
- }
212
-
213
- private registerIdentity(tracked: TrackedEntity): void {
214
- if (tracked.pk == null) return;
215
- this.identityMap.register(tracked);
216
- }
217
-
218
- private createSnapshot(table: TableDef, entity: any): Record<string, any> {
218
+ private async executeCompiled(compiled: CompiledQuery): Promise<QueryResult[]> {
219
+ return this.executor.executeSql(compiled.sql, compiled.params);
220
+ }
221
+
222
+ private getReturningColumns(table: TableDef): ColumnNode[] {
223
+ return Object.values(table.columns).map(column => ({
224
+ type: 'Column',
225
+ table: table.name,
226
+ name: column.name,
227
+ alias: column.name
228
+ }));
229
+ }
230
+
231
+ private applyReturningResults(tracked: TrackedEntity, results: QueryResult[]): void {
232
+ if (!this.dialect.supportsReturning()) return;
233
+ const first = results[0];
234
+ if (!first || first.values.length === 0) return;
235
+
236
+ const row = first.values[0];
237
+ for (let i = 0; i < first.columns.length; i++) {
238
+ const columnName = this.normalizeColumnName(first.columns[i]);
239
+ if (!(columnName in tracked.table.columns)) continue;
240
+ tracked.entity[columnName] = row[i];
241
+ }
242
+ }
243
+
244
+ private normalizeColumnName(column: string): string {
245
+ const parts = column.split('.');
246
+ const candidate = parts[parts.length - 1];
247
+ return candidate.replace(/^["`[\]]+|["`[\]]+$/g, '');
248
+ }
249
+
250
+ private registerIdentity(tracked: TrackedEntity): void {
251
+ if (tracked.pk == null) return;
252
+ this.identityMap.register(tracked);
253
+ }
254
+
255
+ private createSnapshot(table: TableDef, entity: any): Record<string, any> {
219
256
  const snapshot: Record<string, any> = {};
220
257
  for (const column of Object.keys(table.columns)) {
221
258
  snapshot[column] = entity[column];
@@ -1,8 +1,11 @@
1
- import { TableDef } from '../schema/table.js';
2
- import { RelationDef } from '../schema/relation.js';
3
- import { SelectQueryNode, HydrationPlan } from '../core/ast/query.js';
4
- import { HydrationPlanner } from './hydration-planner.js';
5
- import { SelectQueryState, ProjectionNode } from './select-query-state.js';
1
+ import { TableDef } from '../schema/table.js';
2
+ import { RelationDef, RelationKinds } from '../schema/relation.js';
3
+ import { CommonTableExpressionNode, HydrationPlan, OrderByNode, SelectQueryNode } from '../core/ast/query.js';
4
+ import { HydrationPlanner } from './hydration-planner.js';
5
+ import { ProjectionNode, SelectQueryState } from './select-query-state.js';
6
+ import { ColumnNode, eq } from '../core/ast/expression.js';
7
+ import { createJoinNode } from '../core/ast/join-node.js';
8
+ import { JOIN_KINDS } from '../core/sql/sql.js';
6
9
 
7
10
  /**
8
11
  * Manages hydration planning for query results
@@ -60,28 +63,229 @@ export class HydrationManager {
60
63
  return this.clone(next);
61
64
  }
62
65
 
63
- /**
64
- * Applies hydration plan to the AST
65
- * @param ast - Query AST to modify
66
- * @returns AST with hydration metadata
67
- */
68
- applyToAst(ast: SelectQueryNode): SelectQueryNode {
69
- const plan = this.planner.getPlan();
70
- if (!plan) return ast;
71
- return {
72
- ...ast,
73
- meta: {
74
- ...(ast.meta || {}),
75
- hydration: plan
76
- }
77
- };
78
- }
66
+ /**
67
+ * Applies hydration plan to the AST
68
+ * @param ast - Query AST to modify
69
+ * @returns AST with hydration metadata
70
+ */
71
+ applyToAst(ast: SelectQueryNode): SelectQueryNode {
72
+ // Hydration is not applied to compound set queries since row identity is ambiguous.
73
+ if (ast.setOps && ast.setOps.length > 0) {
74
+ return ast;
75
+ }
76
+
77
+ const plan = this.planner.getPlan();
78
+ if (!plan) return ast;
79
+
80
+ const needsPaginationGuard = this.requiresParentPagination(ast, plan);
81
+ const rewritten = needsPaginationGuard ? this.wrapForParentPagination(ast, plan) : ast;
82
+ return this.attachHydrationMeta(rewritten, plan);
83
+ }
79
84
 
80
85
  /**
81
86
  * Gets the current hydration plan
82
87
  * @returns Hydration plan or undefined if none exists
83
88
  */
84
- getPlan(): HydrationPlan | undefined {
85
- return this.planner.getPlan();
86
- }
87
- }
89
+ getPlan(): HydrationPlan | undefined {
90
+ return this.planner.getPlan();
91
+ }
92
+
93
+ /**
94
+ * Attaches hydration metadata to a query AST node.
95
+ */
96
+ private attachHydrationMeta(ast: SelectQueryNode, plan: HydrationPlan): SelectQueryNode {
97
+ return {
98
+ ...ast,
99
+ meta: {
100
+ ...(ast.meta || {}),
101
+ hydration: plan
102
+ }
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Determines whether the query needs pagination rewriting to keep LIMIT/OFFSET
108
+ * applied to parent rows when eager-loading multiplicative relations.
109
+ */
110
+ private requiresParentPagination(ast: SelectQueryNode, plan: HydrationPlan): boolean {
111
+ const hasPagination = ast.limit !== undefined || ast.offset !== undefined;
112
+ return hasPagination && this.hasMultiplyingRelations(plan);
113
+ }
114
+
115
+ private hasMultiplyingRelations(plan: HydrationPlan): boolean {
116
+ return plan.relations.some(
117
+ rel => rel.type === RelationKinds.HasMany || rel.type === RelationKinds.BelongsToMany
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Rewrites the query using CTEs so LIMIT/OFFSET target distinct parent rows
123
+ * instead of the joined result set.
124
+ *
125
+ * The strategy:
126
+ * - Hoist the original query (minus limit/offset) into a base CTE.
127
+ * - Select distinct parent ids from that base CTE with the original ordering and pagination.
128
+ * - Join the base CTE against the paged ids to retrieve the joined rows for just that page.
129
+ */
130
+ private wrapForParentPagination(ast: SelectQueryNode, plan: HydrationPlan): SelectQueryNode {
131
+ const projectionNames = this.getProjectionNames(ast.columns);
132
+ if (!projectionNames) {
133
+ return ast;
134
+ }
135
+
136
+ const projectionAliases = this.buildProjectionAliasMap(ast.columns);
137
+ const projectionSet = new Set(projectionNames);
138
+ const rootPkAlias = projectionAliases.get(`${plan.rootTable}.${plan.rootPrimaryKey}`) ?? plan.rootPrimaryKey;
139
+
140
+ const baseCteName = this.nextCteName(ast.ctes, '__metal_pagination_base');
141
+ const baseQuery: SelectQueryNode = {
142
+ ...ast,
143
+ ctes: undefined,
144
+ limit: undefined,
145
+ offset: undefined,
146
+ orderBy: undefined,
147
+ meta: undefined
148
+ };
149
+
150
+ const baseCte: CommonTableExpressionNode = {
151
+ type: 'CommonTableExpression',
152
+ name: baseCteName,
153
+ query: baseQuery,
154
+ recursive: false
155
+ };
156
+
157
+ const orderBy = this.mapOrderBy(ast.orderBy, plan, projectionAliases, baseCteName, projectionSet);
158
+ // When an order-by uses child-table columns we cannot safely rewrite pagination,
159
+ // so preserve the original query to avoid changing semantics.
160
+ if (orderBy === null) {
161
+ return ast;
162
+ }
163
+
164
+ const pageCteName = this.nextCteName([...(ast.ctes ?? []), baseCte], '__metal_pagination_page');
165
+ const pagingColumns = this.buildPagingColumns(rootPkAlias, orderBy, baseCteName);
166
+
167
+ const pageCte: CommonTableExpressionNode = {
168
+ type: 'CommonTableExpression',
169
+ name: pageCteName,
170
+ query: {
171
+ type: 'SelectQuery',
172
+ from: { type: 'Table', name: baseCteName },
173
+ columns: pagingColumns,
174
+ joins: [],
175
+ distinct: [{ type: 'Column', table: baseCteName, name: rootPkAlias }],
176
+ orderBy,
177
+ limit: ast.limit,
178
+ offset: ast.offset
179
+ },
180
+ recursive: false
181
+ };
182
+
183
+ const joinCondition = eq(
184
+ { type: 'Column', table: baseCteName, name: rootPkAlias },
185
+ { type: 'Column', table: pageCteName, name: rootPkAlias }
186
+ );
187
+
188
+ const outerColumns: ColumnNode[] = projectionNames.map(name => ({
189
+ type: 'Column',
190
+ table: baseCteName,
191
+ name,
192
+ alias: name
193
+ }));
194
+
195
+ return {
196
+ type: 'SelectQuery',
197
+ from: { type: 'Table', name: baseCteName },
198
+ columns: outerColumns,
199
+ joins: [createJoinNode(JOIN_KINDS.INNER, pageCteName, joinCondition)],
200
+ orderBy,
201
+ ctes: [...(ast.ctes ?? []), baseCte, pageCte]
202
+ };
203
+ }
204
+
205
+ private nextCteName(existing: CommonTableExpressionNode[] | undefined, baseName: string): string {
206
+ const names = new Set((existing ?? []).map(cte => cte.name));
207
+ let candidate = baseName;
208
+ let suffix = 1;
209
+
210
+ while (names.has(candidate)) {
211
+ suffix += 1;
212
+ candidate = `${baseName}_${suffix}`;
213
+ }
214
+
215
+ return candidate;
216
+ }
217
+
218
+ private getProjectionNames(columns: ProjectionNode[]): string[] | undefined {
219
+ const names: string[] = [];
220
+ for (const col of columns) {
221
+ const alias = (col as any).alias ?? (col as any).name;
222
+ if (!alias) return undefined;
223
+ names.push(alias);
224
+ }
225
+ return names;
226
+ }
227
+
228
+ private buildProjectionAliasMap(columns: ProjectionNode[]): Map<string, string> {
229
+ const map = new Map<string, string>();
230
+ for (const col of columns) {
231
+ if ((col as ColumnNode).type !== 'Column') continue;
232
+ const node = col as ColumnNode;
233
+ const key = `${node.table}.${node.name}`;
234
+ map.set(key, node.alias ?? node.name);
235
+ }
236
+ return map;
237
+ }
238
+
239
+ private mapOrderBy(
240
+ orderBy: OrderByNode[] | undefined,
241
+ plan: HydrationPlan,
242
+ projectionAliases: Map<string, string>,
243
+ baseAlias: string,
244
+ availableColumns: Set<string>
245
+ ): OrderByNode[] | undefined | null {
246
+ if (!orderBy || orderBy.length === 0) {
247
+ return undefined;
248
+ }
249
+
250
+ const mapped: OrderByNode[] = [];
251
+
252
+ for (const ob of orderBy) {
253
+ // Only rewrite when ordering by root columns; child columns would reintroduce the pagination bug.
254
+ if (ob.column.table !== plan.rootTable) {
255
+ return null;
256
+ }
257
+
258
+ const alias = projectionAliases.get(`${ob.column.table}.${ob.column.name}`) ?? ob.column.name;
259
+ if (!availableColumns.has(alias)) {
260
+ return null;
261
+ }
262
+
263
+ mapped.push({
264
+ type: 'OrderBy',
265
+ column: { type: 'Column', table: baseAlias, name: alias },
266
+ direction: ob.direction
267
+ });
268
+ }
269
+
270
+ return mapped;
271
+ }
272
+
273
+ private buildPagingColumns(primaryKey: string, orderBy: OrderByNode[] | undefined, tableAlias: string): ColumnNode[] {
274
+ const columns: ColumnNode[] = [{ type: 'Column', table: tableAlias, name: primaryKey, alias: primaryKey }];
275
+
276
+ if (!orderBy) return columns;
277
+
278
+ for (const ob of orderBy) {
279
+ if (!columns.some(col => col.name === ob.column.name)) {
280
+ columns.push({
281
+ type: 'Column',
282
+ table: tableAlias,
283
+ name: ob.column.name,
284
+ alias: ob.column.name
285
+ });
286
+ }
287
+ }
288
+
289
+ return columns;
290
+ }
291
+ }
@@ -1,6 +1,6 @@
1
1
  import { TableDef } from '../schema/table.js';
2
2
  import { ColumnDef } from '../schema/column.js';
3
- import { SelectQueryNode, CommonTableExpressionNode } from '../core/ast/query.js';
3
+ import { SelectQueryNode, CommonTableExpressionNode, SetOperationKind, SetOperationNode } from '../core/ast/query.js';
4
4
  import { buildColumnNode } from '../core/ast/builders.js';
5
5
  import {
6
6
  ColumnNode,
@@ -95,17 +95,32 @@ export class QueryAstService {
95
95
  * @param recursive - Whether the CTE is recursive
96
96
  * @returns Updated query state with CTE
97
97
  */
98
- withCte(name: string, query: SelectQueryNode, columns?: string[], recursive = false): SelectQueryState {
99
- const cte: CommonTableExpressionNode = {
100
- type: 'CommonTableExpression',
101
- name,
102
- query,
103
- columns,
104
- recursive
105
- };
106
-
107
- return this.state.withCte(cte);
108
- }
98
+ withCte(name: string, query: SelectQueryNode, columns?: string[], recursive = false): SelectQueryState {
99
+ const cte: CommonTableExpressionNode = {
100
+ type: 'CommonTableExpression',
101
+ name,
102
+ query,
103
+ columns,
104
+ recursive
105
+ };
106
+
107
+ return this.state.withCte(cte);
108
+ }
109
+
110
+ /**
111
+ * Adds a set operation (UNION/UNION ALL/INTERSECT/EXCEPT) to the query
112
+ * @param operator - Set operator
113
+ * @param query - Right-hand side query
114
+ * @returns Updated query state with set operation
115
+ */
116
+ withSetOperation(operator: SetOperationKind, query: SelectQueryNode): SelectQueryState {
117
+ const op: SetOperationNode = {
118
+ type: 'SetOperation',
119
+ operator,
120
+ query
121
+ };
122
+ return this.state.withSetOperation(op);
123
+ }
109
124
 
110
125
  /**
111
126
  * Selects a subquery as a column
@@ -1,5 +1,5 @@
1
1
  import { TableDef } from '../schema/table.js';
2
- import { SelectQueryNode, CommonTableExpressionNode, OrderByNode } from '../core/ast/query.js';
2
+ import { SelectQueryNode, CommonTableExpressionNode, OrderByNode, SetOperationNode } from '../core/ast/query.js';
3
3
  import {
4
4
  ColumnNode,
5
5
  ExpressionNode,
@@ -166,14 +166,26 @@ export class SelectQueryState {
166
166
  }
167
167
 
168
168
  /**
169
- * Adds a Common Table Expression (CTE) to the query
170
- * @param cte - CTE node to add
171
- * @returns New SelectQueryState with CTE
172
- */
173
- withCte(cte: CommonTableExpressionNode): SelectQueryState {
174
- return this.clone({
175
- ...this.ast,
176
- ctes: [...(this.ast.ctes ?? []), cte]
177
- });
178
- }
179
- }
169
+ * Adds a Common Table Expression (CTE) to the query
170
+ * @param cte - CTE node to add
171
+ * @returns New SelectQueryState with CTE
172
+ */
173
+ withCte(cte: CommonTableExpressionNode): SelectQueryState {
174
+ return this.clone({
175
+ ...this.ast,
176
+ ctes: [...(this.ast.ctes ?? []), cte]
177
+ });
178
+ }
179
+
180
+ /**
181
+ * Adds a set operation (UNION/INTERSECT/EXCEPT) to the query
182
+ * @param op - Set operation node to add
183
+ * @returns New SelectQueryState with set operation
184
+ */
185
+ withSetOperation(op: SetOperationNode): SelectQueryState {
186
+ return this.clone({
187
+ ...this.ast,
188
+ setOps: [...(this.ast.setOps ?? []), op]
189
+ });
190
+ }
191
+ }
@@ -1,6 +1,6 @@
1
1
  import { TableDef } from '../schema/table.js';
2
2
  import { ColumnDef } from '../schema/column.js';
3
- import { SelectQueryNode, HydrationPlan } from '../core/ast/query.js';
3
+ import { SelectQueryNode, HydrationPlan, SetOperationKind } from '../core/ast/query.js';
4
4
  import {
5
5
  ColumnNode,
6
6
  ExpressionNode,
@@ -96,15 +96,23 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
96
96
  return { state: nextState, hydration: context.hydration };
97
97
  }
98
98
 
99
- private applyJoin(
100
- context: SelectQueryBuilderContext,
101
- table: TableDef,
102
- condition: BinaryExpressionNode,
103
- kind: JoinKind
104
- ): SelectQueryBuilderContext {
105
- const joinNode = createJoinNode(kind, table.name, condition);
106
- return this.applyAst(context, service => service.withJoin(joinNode));
107
- }
99
+ private applyJoin(
100
+ context: SelectQueryBuilderContext,
101
+ table: TableDef,
102
+ condition: BinaryExpressionNode,
103
+ kind: JoinKind
104
+ ): SelectQueryBuilderContext {
105
+ const joinNode = createJoinNode(kind, table.name, condition);
106
+ return this.applyAst(context, service => service.withJoin(joinNode));
107
+ }
108
+
109
+ private applySetOperation(
110
+ operator: SetOperationKind,
111
+ query: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode
112
+ ): SelectQueryBuilderContext {
113
+ const subAst = this.resolveQueryNode(query);
114
+ return this.applyAst(this.context, service => service.withSetOperation(operator, subAst));
115
+ }
108
116
 
109
117
  /**
110
118
  * Selects specific columns for the query
@@ -315,10 +323,46 @@ export class SelectQueryBuilder<T = any, TTable extends TableDef = TableDef> {
315
323
  * @param n - Number of rows to skip
316
324
  * @returns New query builder instance with the OFFSET clause
317
325
  */
318
- offset(n: number): SelectQueryBuilder<T, TTable> {
319
- const nextContext = this.applyAst(this.context, service => service.withOffset(n));
320
- return this.clone(nextContext);
321
- }
326
+ offset(n: number): SelectQueryBuilder<T, TTable> {
327
+ const nextContext = this.applyAst(this.context, service => service.withOffset(n));
328
+ return this.clone(nextContext);
329
+ }
330
+
331
+ /**
332
+ * Combines this query with another using UNION
333
+ * @param query - Query to union with
334
+ * @returns New query builder instance with the set operation
335
+ */
336
+ union(query: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
337
+ return this.clone(this.applySetOperation('UNION', query));
338
+ }
339
+
340
+ /**
341
+ * Combines this query with another using UNION ALL
342
+ * @param query - Query to union with
343
+ * @returns New query builder instance with the set operation
344
+ */
345
+ unionAll(query: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
346
+ return this.clone(this.applySetOperation('UNION ALL', query));
347
+ }
348
+
349
+ /**
350
+ * Combines this query with another using INTERSECT
351
+ * @param query - Query to intersect with
352
+ * @returns New query builder instance with the set operation
353
+ */
354
+ intersect(query: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
355
+ return this.clone(this.applySetOperation('INTERSECT', query));
356
+ }
357
+
358
+ /**
359
+ * Combines this query with another using EXCEPT
360
+ * @param query - Query to subtract
361
+ * @returns New query builder instance with the set operation
362
+ */
363
+ except(query: SelectQueryBuilder<any, TableDef<any>> | SelectQueryNode): SelectQueryBuilder<T, TTable> {
364
+ return this.clone(this.applySetOperation('EXCEPT', query));
365
+ }
322
366
 
323
367
  /**
324
368
  * Adds a WHERE EXISTS condition to the query