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.
- package/README.md +21 -18
- package/dist/decorators/index.cjs +317 -34
- package/dist/decorators/index.cjs.map +1 -1
- package/dist/decorators/index.d.cts +1 -1
- package/dist/decorators/index.d.ts +1 -1
- package/dist/decorators/index.js +317 -34
- package/dist/decorators/index.js.map +1 -1
- package/dist/index.cjs +1965 -267
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +273 -23
- package/dist/index.d.ts +273 -23
- package/dist/index.js +1947 -267
- package/dist/index.js.map +1 -1
- package/dist/{select-654m4qy8.d.cts → select-CCp1oz9p.d.cts} +254 -4
- package/dist/{select-654m4qy8.d.ts → select-CCp1oz9p.d.ts} +254 -4
- package/package.json +3 -2
- package/src/core/ast/query.ts +40 -22
- package/src/core/ddl/dialects/base-schema-dialect.ts +48 -0
- package/src/core/ddl/dialects/index.ts +5 -0
- package/src/core/ddl/dialects/mssql-schema-dialect.ts +97 -0
- package/src/core/ddl/dialects/mysql-schema-dialect.ts +109 -0
- package/src/core/ddl/dialects/postgres-schema-dialect.ts +99 -0
- package/src/core/ddl/dialects/sqlite-schema-dialect.ts +103 -0
- package/src/core/ddl/introspect/mssql.ts +149 -0
- package/src/core/ddl/introspect/mysql.ts +99 -0
- package/src/core/ddl/introspect/postgres.ts +154 -0
- package/src/core/ddl/introspect/sqlite.ts +66 -0
- package/src/core/ddl/introspect/types.ts +19 -0
- package/src/core/ddl/introspect/utils.ts +27 -0
- package/src/core/ddl/schema-diff.ts +179 -0
- package/src/core/ddl/schema-generator.ts +229 -0
- package/src/core/ddl/schema-introspect.ts +32 -0
- package/src/core/ddl/schema-types.ts +39 -0
- package/src/core/dialect/abstract.ts +122 -37
- package/src/core/dialect/base/sql-dialect.ts +204 -0
- package/src/core/dialect/mssql/index.ts +125 -80
- package/src/core/dialect/mysql/index.ts +18 -112
- package/src/core/dialect/postgres/index.ts +29 -126
- package/src/core/dialect/sqlite/index.ts +28 -129
- package/src/index.ts +4 -0
- package/src/orm/execute.ts +25 -16
- package/src/orm/orm-context.ts +60 -55
- package/src/orm/query-logger.ts +38 -0
- package/src/orm/relations/belongs-to.ts +42 -26
- package/src/orm/relations/has-many.ts +41 -25
- package/src/orm/relations/many-to-many.ts +43 -27
- package/src/orm/unit-of-work.ts +60 -23
- package/src/query-builder/hydration-manager.ts +229 -25
- package/src/query-builder/query-ast-service.ts +27 -12
- package/src/query-builder/select-query-state.ts +24 -12
- package/src/query-builder/select.ts +58 -14
- package/src/schema/column.ts +206 -27
- package/src/schema/table.ts +89 -32
- package/src/schema/types.ts +8 -5
package/src/orm/unit-of-work.ts
CHANGED
|
@@ -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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
150
|
-
.set(changes)
|
|
151
|
-
.where(eq(pkColumn, tracked.pk));
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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<
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
private
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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 {
|
|
4
|
-
import { HydrationPlanner } from './hydration-planner.js';
|
|
5
|
-
import {
|
|
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
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|