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.
@@ -1,4 +1,11 @@
1
- import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../ast/query.js';
1
+ import {
2
+ SelectQueryNode,
3
+ InsertQueryNode,
4
+ UpdateQueryNode,
5
+ DeleteQueryNode,
6
+ SetOperationKind,
7
+ CommonTableExpressionNode
8
+ } from '../ast/query.js';
2
9
  import {
3
10
  ExpressionNode,
4
11
  BinaryExpressionNode,
@@ -56,22 +63,23 @@ export interface DeleteCompiler {
56
63
  /**
57
64
  * Abstract base class for SQL dialect implementations
58
65
  */
59
- export abstract class Dialect
60
- implements SelectCompiler, InsertCompiler, UpdateCompiler, DeleteCompiler
61
- {
66
+ export abstract class Dialect
67
+ implements SelectCompiler, InsertCompiler, UpdateCompiler, DeleteCompiler
68
+ {
62
69
  /**
63
70
  * Compiles a SELECT query AST to SQL
64
71
  * @param ast - Query AST to compile
65
72
  * @returns Compiled query with SQL and parameters
66
73
  */
67
- compileSelect(ast: SelectQueryNode): CompiledQuery {
68
- const ctx = this.createCompilerContext();
69
- const rawSql = this.compileSelectAst(ast, ctx).trim();
70
- const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
71
- return {
72
- sql,
73
- params: [...ctx.params]
74
- };
74
+ compileSelect(ast: SelectQueryNode): CompiledQuery {
75
+ const ctx = this.createCompilerContext();
76
+ const normalized = this.normalizeSelectAst(ast);
77
+ const rawSql = this.compileSelectAst(normalized, ctx).trim();
78
+ const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
79
+ return {
80
+ sql,
81
+ params: [...ctx.params]
82
+ };
75
83
  }
76
84
 
77
85
  compileInsert(ast: InsertQueryNode): CompiledQuery {
@@ -94,15 +102,19 @@ export abstract class Dialect
94
102
  };
95
103
  }
96
104
 
97
- compileDelete(ast: DeleteQueryNode): CompiledQuery {
98
- const ctx = this.createCompilerContext();
99
- const rawSql = this.compileDeleteAst(ast, ctx).trim();
105
+ compileDelete(ast: DeleteQueryNode): CompiledQuery {
106
+ const ctx = this.createCompilerContext();
107
+ const rawSql = this.compileDeleteAst(ast, ctx).trim();
100
108
  const sql = rawSql.endsWith(';') ? rawSql : `${rawSql};`;
101
109
  return {
102
110
  sql,
103
111
  params: [...ctx.params]
104
- };
105
- }
112
+ };
113
+ }
114
+
115
+ supportsReturning(): boolean {
116
+ return false;
117
+ }
106
118
 
107
119
  /**
108
120
  * Compiles SELECT query AST to SQL (to be implemented by concrete dialects)
@@ -110,7 +122,7 @@ export abstract class Dialect
110
122
  * @param ctx - Compiler context
111
123
  * @returns SQL string
112
124
  */
113
- protected abstract compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string;
125
+ protected abstract compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string;
114
126
 
115
127
  protected abstract compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string;
116
128
  protected abstract compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string;
@@ -149,16 +161,23 @@ export abstract class Dialect
149
161
  * Does not add ';' at the end
150
162
  * @param ast - Query AST
151
163
  * @param ctx - Compiler context
152
- * @returns SQL for EXISTS subquery
153
- */
154
- protected compileSelectForExists(ast: SelectQueryNode, ctx: CompilerContext): string {
155
- const full = this.compileSelectAst(ast, ctx).trim().replace(/;$/, '');
156
- const upper = full.toUpperCase();
157
- const fromIndex = upper.indexOf(' FROM ');
158
- if (fromIndex === -1) {
159
- return full;
160
- }
161
-
164
+ * @returns SQL for EXISTS subquery
165
+ */
166
+ protected compileSelectForExists(ast: SelectQueryNode, ctx: CompilerContext): string {
167
+ const normalized = this.normalizeSelectAst(ast);
168
+ const full = this.compileSelectAst(normalized, ctx).trim().replace(/;$/, '');
169
+
170
+ // When the subquery is a set operation, wrap it as a derived table to keep valid syntax.
171
+ if (normalized.setOps && normalized.setOps.length > 0) {
172
+ return `SELECT 1 FROM (${full}) AS _exists`;
173
+ }
174
+
175
+ const upper = full.toUpperCase();
176
+ const fromIndex = upper.indexOf(' FROM ');
177
+ if (fromIndex === -1) {
178
+ return full;
179
+ }
180
+
162
181
  const tail = full.slice(fromIndex);
163
182
  return `SELECT 1${tail}`;
164
183
  }
@@ -185,15 +204,81 @@ export abstract class Dialect
185
204
  * @param index - Parameter index
186
205
  * @returns Formatted placeholder string
187
206
  */
188
- protected formatPlaceholder(index: number): string {
189
- return '?';
190
- }
191
-
192
- private readonly expressionCompilers: Map<string, (node: ExpressionNode, ctx: CompilerContext) => string>;
193
- private readonly operandCompilers: Map<string, (node: OperandNode, ctx: CompilerContext) => string>;
194
-
195
- protected constructor() {
196
- this.expressionCompilers = new Map();
207
+ protected formatPlaceholder(index: number): string {
208
+ return '?';
209
+ }
210
+
211
+ /**
212
+ * Whether the current dialect supports a given set operation.
213
+ * Override in concrete dialects to restrict support.
214
+ */
215
+ protected supportsSetOperation(kind: SetOperationKind): boolean {
216
+ return true;
217
+ }
218
+
219
+ /**
220
+ * Validates set-operation semantics:
221
+ * - Ensures the dialect supports requested operators.
222
+ * - Enforces that only the outermost compound query may have ORDER/LIMIT/OFFSET.
223
+ * @param ast - Query to validate
224
+ * @param isOutermost - Whether this node is the outermost compound query
225
+ */
226
+ protected validateSetOperations(ast: SelectQueryNode, isOutermost = true): void {
227
+ const hasSetOps = !!(ast.setOps && ast.setOps.length);
228
+ if (!isOutermost && (ast.orderBy || ast.limit !== undefined || ast.offset !== undefined)) {
229
+ throw new Error('ORDER BY / LIMIT / OFFSET are only allowed on the outermost compound query.');
230
+ }
231
+
232
+ if (hasSetOps) {
233
+ for (const op of ast.setOps!) {
234
+ if (!this.supportsSetOperation(op.operator)) {
235
+ throw new Error(`Set operation ${op.operator} is not supported by this dialect.`);
236
+ }
237
+ this.validateSetOperations(op.query, false);
238
+ }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Hoists CTEs from set-operation operands to the outermost query so WITH appears once.
244
+ * @param ast - Query AST
245
+ * @returns Normalized AST without inner CTEs and a list of hoisted CTEs
246
+ */
247
+ private hoistCtes(ast: SelectQueryNode): { normalized: SelectQueryNode; hoistedCtes: CommonTableExpressionNode[] } {
248
+ let hoisted: CommonTableExpressionNode[] = [];
249
+
250
+ const normalizedSetOps = ast.setOps?.map(op => {
251
+ const { normalized: child, hoistedCtes: childHoisted } = this.hoistCtes(op.query);
252
+ const childCtes = child.ctes ?? [];
253
+ if (childCtes.length) {
254
+ hoisted = hoisted.concat(childCtes);
255
+ }
256
+ hoisted = hoisted.concat(childHoisted);
257
+ const queryWithoutCtes = childCtes.length ? { ...child, ctes: undefined } : child;
258
+ return { ...op, query: queryWithoutCtes };
259
+ });
260
+
261
+ const normalized: SelectQueryNode = normalizedSetOps ? { ...ast, setOps: normalizedSetOps } : ast;
262
+ return { normalized, hoistedCtes: hoisted };
263
+ }
264
+
265
+ /**
266
+ * Normalizes a SELECT AST before compilation (validation + CTE hoisting).
267
+ * @param ast - Query AST
268
+ * @returns Normalized query AST
269
+ */
270
+ protected normalizeSelectAst(ast: SelectQueryNode): SelectQueryNode {
271
+ this.validateSetOperations(ast, true);
272
+ const { normalized, hoistedCtes } = this.hoistCtes(ast);
273
+ const combinedCtes = [...(normalized.ctes ?? []), ...hoistedCtes];
274
+ return combinedCtes.length ? { ...normalized, ctes: combinedCtes } : normalized;
275
+ }
276
+
277
+ private readonly expressionCompilers: Map<string, (node: ExpressionNode, ctx: CompilerContext) => string>;
278
+ private readonly operandCompilers: Map<string, (node: OperandNode, ctx: CompilerContext) => string>;
279
+
280
+ protected constructor() {
281
+ this.expressionCompilers = new Map();
197
282
  this.operandCompilers = new Map();
198
283
  this.registerDefaultOperandCompilers();
199
284
  this.registerDefaultExpressionCompilers();
@@ -18,17 +18,29 @@ export abstract class SqlDialectBase extends Dialect {
18
18
  * Compiles SELECT query AST to SQL using common rules.
19
19
  */
20
20
  protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
21
+ const hasSetOps = !!(ast.setOps && ast.setOps.length);
21
22
  const ctes = this.compileCtes(ast, ctx);
22
- const columns = this.compileSelectColumns(ast, ctx);
23
- const from = this.compileFrom(ast.from);
24
- const joins = this.compileJoins(ast, ctx);
25
- const whereClause = this.compileWhere(ast.where, ctx);
26
- const groupBy = this.compileGroupBy(ast);
27
- const having = this.compileHaving(ast, ctx);
23
+
24
+ // When set operations exist, omit ORDER BY/OFFSET/LIMIT from the operands and apply at the end.
25
+ const baseAst: SelectQueryNode = hasSetOps
26
+ ? { ...ast, setOps: undefined, orderBy: undefined, limit: undefined, offset: undefined }
27
+ : ast;
28
+
29
+ const baseSelect = this.compileSelectCore(baseAst, ctx);
30
+
31
+ if (!hasSetOps) {
32
+ return `${ctes}${baseSelect}`;
33
+ }
34
+
35
+ const compound = ast.setOps!
36
+ .map(op => `${op.operator} ${this.wrapSetOperand(this.compileSelectAst(op.query, ctx))}`)
37
+ .join(' ');
38
+
28
39
  const orderBy = this.compileOrderBy(ast);
29
40
  const pagination = this.compilePagination(ast, orderBy);
30
41
 
31
- return `${ctes}SELECT ${this.compileDistinct(ast)}${columns} FROM ${from}${joins}${whereClause}${groupBy}${having}${orderBy}${pagination}`;
42
+ const combined = `${this.wrapSetOperand(baseSelect)} ${compound}`;
43
+ return `${ctes}${combined}${orderBy}${pagination}`;
32
44
  }
33
45
 
34
46
  protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
@@ -41,6 +53,22 @@ export abstract class SqlDialectBase extends Dialect {
41
53
  return `INSERT INTO ${table} (${columnList}) VALUES ${values}${returning}`;
42
54
  }
43
55
 
56
+ /**
57
+ * Compiles a single SELECT (no set operations, no CTE prefix).
58
+ */
59
+ private compileSelectCore(ast: SelectQueryNode, ctx: CompilerContext): string {
60
+ const columns = this.compileSelectColumns(ast, ctx);
61
+ const from = this.compileFrom(ast.from);
62
+ const joins = this.compileJoins(ast, ctx);
63
+ const whereClause = this.compileWhere(ast.where, ctx);
64
+ const groupBy = this.compileGroupBy(ast);
65
+ const having = this.compileHaving(ast, ctx);
66
+ const orderBy = this.compileOrderBy(ast);
67
+ const pagination = this.compilePagination(ast, orderBy);
68
+
69
+ return `SELECT ${this.compileDistinct(ast)}${columns} FROM ${from}${joins}${whereClause}${groupBy}${having}${orderBy}${pagination}`;
70
+ }
71
+
44
72
  protected compileUpdateAst(ast: UpdateQueryNode, ctx: CompilerContext): string {
45
73
  const table = this.compileTableName(ast.table);
46
74
  const assignments = ast.set.map(assignment => {
@@ -69,6 +97,16 @@ export abstract class SqlDialectBase extends Dialect {
69
97
  throw new Error('RETURNING is not supported by this dialect.');
70
98
  }
71
99
 
100
+ protected formatReturningColumns(returning: ColumnNode[]): string {
101
+ return returning
102
+ .map(column => {
103
+ const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : '';
104
+ const aliasPart = column.alias ? ` AS ${this.quoteIdentifier(column.alias)}` : '';
105
+ return `${tablePart}${this.quoteIdentifier(column.name)}${aliasPart}`;
106
+ })
107
+ .join(', ');
108
+ }
109
+
72
110
  /**
73
111
  * DISTINCT clause. Override for DISTINCT ON support.
74
112
  */
@@ -149,7 +187,7 @@ export abstract class SqlDialectBase extends Dialect {
149
187
  const cols = cte.columns && cte.columns.length
150
188
  ? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})`
151
189
  : '';
152
- const query = this.stripTrailingSemicolon(this.compileSelectAst(cte.query, ctx));
190
+ const query = this.stripTrailingSemicolon(this.compileSelectAst(this.normalizeSelectAst(cte.query), ctx));
153
191
  return `${name}${cols} AS (${query})`;
154
192
  }).join(', ');
155
193
  return `${prefix}${cteDefs} `;
@@ -158,4 +196,9 @@ export abstract class SqlDialectBase extends Dialect {
158
196
  protected stripTrailingSemicolon(sql: string): string {
159
197
  return sql.trim().replace(/;$/, '');
160
198
  }
199
+
200
+ protected wrapSetOperand(sql: string): string {
201
+ const trimmed = this.stripTrailingSemicolon(sql);
202
+ return `(${trimmed})`;
203
+ }
161
204
  }
@@ -1,6 +1,6 @@
1
- import { CompilerContext, Dialect } from '../abstract.js';
2
- import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../../ast/query.js';
3
- import { JsonPathNode } from '../../ast/expression.js';
1
+ import { CompilerContext, Dialect } from '../abstract.js';
2
+ import { SelectQueryNode, InsertQueryNode, UpdateQueryNode, DeleteQueryNode } from '../../ast/query.js';
3
+ import { JsonPathNode } from '../../ast/expression.js';
4
4
 
5
5
  /**
6
6
  * Microsoft SQL Server dialect implementation
@@ -42,77 +42,36 @@ export class SqlServerDialect extends Dialect {
42
42
  return `@p${index}`;
43
43
  }
44
44
 
45
- /**
46
- * Compiles SELECT query AST to SQL Server SQL
47
- * @param ast - Query AST
48
- * @param ctx - Compiler context
49
- * @returns SQL Server SQL string
50
- */
51
- protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
52
- const columns = ast.columns.map(c => {
53
- let expr = '';
54
- if (c.type === 'Function') {
55
- expr = this.compileOperand(c, ctx);
56
- } else if (c.type === 'Column') {
57
- expr = `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`;
58
- } else if (c.type === 'ScalarSubquery') {
59
- expr = this.compileOperand(c, ctx);
60
- } else if (c.type === 'WindowFunction') {
61
- expr = this.compileOperand(c, ctx);
62
- }
63
-
64
- if (c.alias) {
65
- if (c.alias.includes('(')) return c.alias;
66
- return `${expr} AS ${this.quoteIdentifier(c.alias)}`;
67
- }
68
- return expr;
69
- }).join(', ');
70
-
71
- const distinct = ast.distinct ? 'DISTINCT ' : '';
72
- const from = `${this.quoteIdentifier(ast.from.name)}`;
73
-
74
- const joins = ast.joins.map(j => {
75
- const table = this.quoteIdentifier(j.table.name);
76
- const cond = this.compileExpression(j.condition, ctx);
77
- return `${j.kind} JOIN ${table} ON ${cond}`;
78
- }).join(' ');
79
- const whereClause = this.compileWhere(ast.where, ctx);
80
-
81
- const groupBy = ast.groupBy && ast.groupBy.length > 0
82
- ? ' GROUP BY ' + ast.groupBy.map(c => `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`).join(', ')
83
- : '';
84
-
85
- const having = ast.having
86
- ? ` HAVING ${this.compileExpression(ast.having, ctx)}`
87
- : '';
88
-
89
- const orderBy = ast.orderBy && ast.orderBy.length > 0
90
- ? ' ORDER BY ' + ast.orderBy.map(o => `${this.quoteIdentifier(o.column.table)}.${this.quoteIdentifier(o.column.name)} ${o.direction}`).join(', ')
91
- : '';
92
-
93
- let pagination = '';
94
- if (ast.limit || ast.offset) {
95
- const off = ast.offset || 0;
96
- const orderClause = orderBy || ' ORDER BY (SELECT NULL)';
97
- pagination = `${orderClause} OFFSET ${off} ROWS`;
98
- if (ast.limit) {
99
- pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
100
- }
101
- return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${pagination};`;
102
- }
103
-
104
- const ctes = ast.ctes && ast.ctes.length > 0
105
- ? 'WITH ' + ast.ctes.map(cte => {
106
- // MSSQL does not use RECURSIVE keyword
107
- const name = this.quoteIdentifier(cte.name);
108
- const cols = cte.columns ? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})` : '';
109
- const query = this.compileSelectAst(cte.query, ctx).trim().replace(/;$/, '');
110
- return `${name}${cols} AS (${query})`;
111
- }).join(', ') + ' '
112
- : '';
113
-
114
- return `${ctes}SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy};`;
115
- }
45
+ /**
46
+ * Compiles SELECT query AST to SQL Server SQL
47
+ * @param ast - Query AST
48
+ * @param ctx - Compiler context
49
+ * @returns SQL Server SQL string
50
+ */
51
+ protected compileSelectAst(ast: SelectQueryNode, ctx: CompilerContext): string {
52
+ const hasSetOps = !!(ast.setOps && ast.setOps.length);
53
+ const ctes = this.compileCtes(ast, ctx);
54
+
55
+ const baseAst: SelectQueryNode = hasSetOps
56
+ ? { ...ast, setOps: undefined, orderBy: undefined, limit: undefined, offset: undefined }
57
+ : ast;
58
+
59
+ const baseSelect = this.compileSelectCore(baseAst, ctx);
60
+
61
+ if (!hasSetOps) {
62
+ return `${ctes}${baseSelect}`;
63
+ }
64
+
65
+ const compound = ast.setOps!
66
+ .map(op => `${op.operator} ${this.wrapSetOperand(this.compileSelectAst(op.query, ctx))}`)
67
+ .join(' ');
68
+
69
+ const orderBy = this.compileOrderBy(ast);
70
+ const pagination = this.compilePagination(ast, orderBy);
71
+ const combined = `${this.wrapSetOperand(baseSelect)} ${compound}`;
72
+ const tail = pagination || orderBy;
73
+ return `${ctes}${combined}${tail}`;
74
+ }
116
75
 
117
76
  protected compileInsertAst(ast: InsertQueryNode, ctx: CompilerContext): string {
118
77
  const table = this.quoteIdentifier(ast.into.name);
@@ -133,9 +92,95 @@ export class SqlServerDialect extends Dialect {
133
92
  return `UPDATE ${table} SET ${assignments}${whereClause};`;
134
93
  }
135
94
 
136
- protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
137
- const table = this.quoteIdentifier(ast.from.name);
138
- const whereClause = this.compileWhere(ast.where, ctx);
139
- return `DELETE FROM ${table}${whereClause};`;
140
- }
141
- }
95
+ protected compileDeleteAst(ast: DeleteQueryNode, ctx: CompilerContext): string {
96
+ const table = this.quoteIdentifier(ast.from.name);
97
+ const whereClause = this.compileWhere(ast.where, ctx);
98
+ return `DELETE FROM ${table}${whereClause};`;
99
+ }
100
+
101
+ private compileSelectCore(ast: SelectQueryNode, ctx: CompilerContext): string {
102
+ const columns = ast.columns.map(c => {
103
+ let expr = '';
104
+ if (c.type === 'Function') {
105
+ expr = this.compileOperand(c, ctx);
106
+ } else if (c.type === 'Column') {
107
+ expr = `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`;
108
+ } else if (c.type === 'ScalarSubquery') {
109
+ expr = this.compileOperand(c, ctx);
110
+ } else if (c.type === 'WindowFunction') {
111
+ expr = this.compileOperand(c, ctx);
112
+ }
113
+
114
+ if (c.alias) {
115
+ if (c.alias.includes('(')) return c.alias;
116
+ return `${expr} AS ${this.quoteIdentifier(c.alias)}`;
117
+ }
118
+ return expr;
119
+ }).join(', ');
120
+
121
+ const distinct = ast.distinct ? 'DISTINCT ' : '';
122
+ const from = `${this.quoteIdentifier(ast.from.name)}`;
123
+
124
+ const joins = ast.joins.map(j => {
125
+ const table = this.quoteIdentifier(j.table.name);
126
+ const cond = this.compileExpression(j.condition, ctx);
127
+ return `${j.kind} JOIN ${table} ON ${cond}`;
128
+ }).join(' ');
129
+ const whereClause = this.compileWhere(ast.where, ctx);
130
+
131
+ const groupBy = ast.groupBy && ast.groupBy.length > 0
132
+ ? ' GROUP BY ' + ast.groupBy.map(c => `${this.quoteIdentifier(c.table)}.${this.quoteIdentifier(c.name)}`).join(', ')
133
+ : '';
134
+
135
+ const having = ast.having
136
+ ? ` HAVING ${this.compileExpression(ast.having, ctx)}`
137
+ : '';
138
+
139
+ const orderBy = this.compileOrderBy(ast);
140
+ const pagination = this.compilePagination(ast, orderBy);
141
+
142
+ if (pagination) {
143
+ return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${pagination}`;
144
+ }
145
+
146
+ return `SELECT ${distinct}${columns} FROM ${from}${joins ? ' ' + joins : ''}${whereClause}${groupBy}${having}${orderBy}`;
147
+ }
148
+
149
+ private compileOrderBy(ast: SelectQueryNode): string {
150
+ if (!ast.orderBy || ast.orderBy.length === 0) return '';
151
+ return ' ORDER BY ' + ast.orderBy
152
+ .map(o => `${this.quoteIdentifier(o.column.table)}.${this.quoteIdentifier(o.column.name)} ${o.direction}`)
153
+ .join(', ');
154
+ }
155
+
156
+ private compilePagination(ast: SelectQueryNode, orderBy: string): string {
157
+ const hasLimit = ast.limit !== undefined;
158
+ const hasOffset = ast.offset !== undefined;
159
+ if (!hasLimit && !hasOffset) return '';
160
+
161
+ const off = ast.offset ?? 0;
162
+ const orderClause = orderBy || ' ORDER BY (SELECT NULL)';
163
+ let pagination = `${orderClause} OFFSET ${off} ROWS`;
164
+ if (hasLimit) {
165
+ pagination += ` FETCH NEXT ${ast.limit} ROWS ONLY`;
166
+ }
167
+ return pagination;
168
+ }
169
+
170
+ private compileCtes(ast: SelectQueryNode, ctx: CompilerContext): string {
171
+ if (!ast.ctes || ast.ctes.length === 0) return '';
172
+ // MSSQL does not use RECURSIVE keyword, but supports recursion when CTE references itself.
173
+ const defs = ast.ctes.map(cte => {
174
+ const name = this.quoteIdentifier(cte.name);
175
+ const cols = cte.columns ? `(${cte.columns.map(c => this.quoteIdentifier(c)).join(', ')})` : '';
176
+ const query = this.compileSelectAst(this.normalizeSelectAst(cte.query), ctx).trim().replace(/;$/, '');
177
+ return `${name}${cols} AS (${query})`;
178
+ }).join(', ');
179
+ return `WITH ${defs} `;
180
+ }
181
+
182
+ private wrapSetOperand(sql: string): string {
183
+ const trimmed = sql.trim().replace(/;$/, '');
184
+ return `(${trimmed})`;
185
+ }
186
+ }
@@ -35,12 +35,11 @@ export class PostgresDialect extends SqlDialectBase {
35
35
 
36
36
  protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
37
37
  if (!returning || returning.length === 0) return '';
38
- const columns = returning
39
- .map(column => {
40
- const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : '';
41
- return `${tablePart}${this.quoteIdentifier(column.name)}`;
42
- })
43
- .join(', ');
38
+ const columns = this.formatReturningColumns(returning);
44
39
  return ` RETURNING ${columns}`;
45
40
  }
41
+
42
+ supportsReturning(): boolean {
43
+ return true;
44
+ }
46
45
  }
@@ -35,12 +35,11 @@ export class SqliteDialect extends SqlDialectBase {
35
35
 
36
36
  protected compileReturning(returning: ColumnNode[] | undefined, ctx: CompilerContext): string {
37
37
  if (!returning || returning.length === 0) return '';
38
- const columns = returning
39
- .map(column => {
40
- const tablePart = column.table ? `${this.quoteIdentifier(column.table)}.` : '';
41
- return `${tablePart}${this.quoteIdentifier(column.name)}`;
42
- })
43
- .join(', ');
38
+ const columns = this.formatReturningColumns(returning);
44
39
  return ` RETURNING ${columns}`;
45
40
  }
41
+
42
+ supportsReturning(): boolean {
43
+ return true;
44
+ }
46
45
  }
@@ -1,9 +1,9 @@
1
1
  import { TableDef } from '../schema/table.js';
2
2
  import { Entity } from '../schema/types.js';
3
- import { hydrateRows } from './hydration.js';
4
- import { OrmContext } from './orm-context.js';
5
- import { SelectQueryBuilder } from '../query-builder/select.js';
6
- import { createEntityFromRow } from './entity.js';
3
+ import { hydrateRows } from './hydration.js';
4
+ import { OrmContext } from './orm-context.js';
5
+ import { SelectQueryBuilder } from '../query-builder/select.js';
6
+ import { createEntityFromRow, createEntityProxy } from './entity.js';
7
7
 
8
8
  type Row = Record<string, any>;
9
9
 
@@ -22,15 +22,24 @@ const flattenResults = (results: { columns: string[]; values: unknown[][] }[]):
22
22
  return rows;
23
23
  };
24
24
 
25
- export async function executeHydrated<TTable extends TableDef>(
26
- ctx: OrmContext,
27
- qb: SelectQueryBuilder<any, TTable>
28
- ): Promise<Entity<TTable>[]> {
29
- const compiled = ctx.dialect.compileSelect(qb.getAST());
30
- const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
31
- const rows = flattenResults(executed);
32
- const hydrated = hydrateRows(rows, qb.getHydrationPlan());
33
- return hydrated.map(row =>
34
- createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
35
- );
36
- }
25
+ export async function executeHydrated<TTable extends TableDef>(
26
+ ctx: OrmContext,
27
+ qb: SelectQueryBuilder<any, TTable>
28
+ ): Promise<Entity<TTable>[]> {
29
+ const ast = qb.getAST();
30
+ const compiled = ctx.dialect.compileSelect(ast);
31
+ const executed = await ctx.executor.executeSql(compiled.sql, compiled.params);
32
+ const rows = flattenResults(executed);
33
+
34
+ // Set-operation queries cannot be reliably hydrated and should not collapse duplicates.
35
+ if (ast.setOps && ast.setOps.length > 0) {
36
+ return rows.map(row =>
37
+ createEntityProxy(ctx, qb.getTable(), row, qb.getLazyRelations())
38
+ );
39
+ }
40
+
41
+ const hydrated = hydrateRows(rows, qb.getHydrationPlan());
42
+ return hydrated.map(row =>
43
+ createEntityFromRow(ctx, qb.getTable(), row, qb.getLazyRelations())
44
+ );
45
+ }