metal-orm 1.0.38 → 1.0.40

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.
@@ -7,63 +7,63 @@ import { ProjectionNode, SelectQueryState } from './select-query-state.js';
7
7
  import { ColumnNode, eq } from '../core/ast/expression.js';
8
8
  import { createJoinNode } from '../core/ast/join-node.js';
9
9
  import { JOIN_KINDS } from '../core/sql/sql.js';
10
-
11
- /**
12
- * Manages hydration planning for query results
13
- */
14
- export class HydrationManager {
15
- /**
16
- * Creates a new HydrationManager instance
17
- * @param table - Table definition
18
- * @param planner - Hydration planner
19
- */
20
- constructor(
21
- private readonly table: TableDef,
22
- private readonly planner: HydrationPlanner
23
- ) {}
24
-
25
- /**
26
- * Creates a new HydrationManager with updated planner
27
- * @param nextPlanner - Updated hydration planner
28
- * @returns New HydrationManager instance
29
- */
30
- private clone(nextPlanner: HydrationPlanner): HydrationManager {
31
- return new HydrationManager(this.table, nextPlanner);
32
- }
33
-
34
- /**
35
- * Handles column selection for hydration planning
36
- * @param state - Current query state
37
- * @param newColumns - Newly selected columns
38
- * @returns Updated HydrationManager with captured columns
39
- */
40
- onColumnsSelected(state: SelectQueryState, newColumns: ProjectionNode[]): HydrationManager {
41
- const updated = this.planner.captureRootColumns(newColumns);
42
- return this.clone(updated);
43
- }
44
-
45
- /**
46
- * Handles relation inclusion for hydration planning
47
- * @param state - Current query state
48
- * @param relation - Relation definition
49
- * @param relationName - Name of the relation
50
- * @param aliasPrefix - Alias prefix for the relation
51
- * @param targetColumns - Target columns to include
52
- * @returns Updated HydrationManager with included relation
53
- */
54
- onRelationIncluded(
55
- state: SelectQueryState,
56
- relation: RelationDef,
57
- relationName: string,
58
- aliasPrefix: string,
59
- targetColumns: string[],
60
- pivot?: { aliasPrefix: string; columns: string[] }
61
- ): HydrationManager {
62
- const withRoots = this.planner.captureRootColumns(state.ast.columns);
63
- const next = withRoots.includeRelation(relation, relationName, aliasPrefix, targetColumns, pivot);
64
- return this.clone(next);
65
- }
66
-
10
+
11
+ /**
12
+ * Manages hydration planning for query results
13
+ */
14
+ export class HydrationManager {
15
+ /**
16
+ * Creates a new HydrationManager instance
17
+ * @param table - Table definition
18
+ * @param planner - Hydration planner
19
+ */
20
+ constructor(
21
+ private readonly table: TableDef,
22
+ private readonly planner: HydrationPlanner
23
+ ) {}
24
+
25
+ /**
26
+ * Creates a new HydrationManager with updated planner
27
+ * @param nextPlanner - Updated hydration planner
28
+ * @returns New HydrationManager instance
29
+ */
30
+ private clone(nextPlanner: HydrationPlanner): HydrationManager {
31
+ return new HydrationManager(this.table, nextPlanner);
32
+ }
33
+
34
+ /**
35
+ * Handles column selection for hydration planning
36
+ * @param state - Current query state
37
+ * @param newColumns - Newly selected columns
38
+ * @returns Updated HydrationManager with captured columns
39
+ */
40
+ onColumnsSelected(state: SelectQueryState, newColumns: ProjectionNode[]): HydrationManager {
41
+ const updated = this.planner.captureRootColumns(newColumns);
42
+ return this.clone(updated);
43
+ }
44
+
45
+ /**
46
+ * Handles relation inclusion for hydration planning
47
+ * @param state - Current query state
48
+ * @param relation - Relation definition
49
+ * @param relationName - Name of the relation
50
+ * @param aliasPrefix - Alias prefix for the relation
51
+ * @param targetColumns - Target columns to include
52
+ * @returns Updated HydrationManager with included relation
53
+ */
54
+ onRelationIncluded(
55
+ state: SelectQueryState,
56
+ relation: RelationDef,
57
+ relationName: string,
58
+ aliasPrefix: string,
59
+ targetColumns: string[],
60
+ pivot?: { aliasPrefix: string; columns: string[] }
61
+ ): HydrationManager {
62
+ const withRoots = this.planner.captureRootColumns(state.ast.columns);
63
+ const next = withRoots.includeRelation(relation, relationName, aliasPrefix, targetColumns, pivot);
64
+ return this.clone(next);
65
+ }
66
+
67
67
  /**
68
68
  * Applies hydration plan to the AST
69
69
  * @param ast - Query AST to modify
@@ -82,11 +82,11 @@ export class HydrationManager {
82
82
  const rewritten = needsPaginationGuard ? this.wrapForParentPagination(ast, plan) : ast;
83
83
  return this.attachHydrationMeta(rewritten, plan);
84
84
  }
85
-
86
- /**
87
- * Gets the current hydration plan
88
- * @returns Hydration plan or undefined if none exists
89
- */
85
+
86
+ /**
87
+ * Gets the current hydration plan
88
+ * @returns Hydration plan or undefined if none exists
89
+ */
90
90
  getPlan(): HydrationPlan | undefined {
91
91
  return this.planner.getPlan();
92
92
  }
@@ -251,38 +251,52 @@ export class HydrationManager {
251
251
  const mapped: OrderByNode[] = [];
252
252
 
253
253
  for (const ob of orderBy) {
254
- // Only rewrite when ordering by root columns; child columns would reintroduce the pagination bug.
255
- if (ob.column.table !== plan.rootTable) {
256
- return null;
257
- }
254
+ const mappedTerm = this.mapOrderingTerm(ob.term, plan, projectionAliases, baseAlias, availableColumns);
255
+ if (!mappedTerm) return null;
258
256
 
259
- const alias = projectionAliases.get(`${ob.column.table}.${ob.column.name}`) ?? ob.column.name;
260
- if (!availableColumns.has(alias)) {
261
- return null;
262
- }
263
-
264
- mapped.push({
265
- type: 'OrderBy',
266
- column: { type: 'Column', table: baseAlias, name: alias },
267
- direction: ob.direction
268
- });
257
+ mapped.push({ ...ob, term: mappedTerm });
269
258
  }
270
259
 
271
260
  return mapped;
272
261
  }
273
262
 
263
+ private mapOrderingTerm(
264
+ term: OrderByNode['term'],
265
+ plan: HydrationPlan,
266
+ projectionAliases: Map<string, string>,
267
+ baseAlias: string,
268
+ availableColumns: Set<string>
269
+ ): OrderByNode['term'] | null {
270
+ if ((term as any).type === 'Column') {
271
+ const col = term as ColumnNode;
272
+ if (col.table !== plan.rootTable) return null;
273
+ const alias = projectionAliases.get(`${col.table}.${col.name}`) ?? col.name;
274
+ if (!availableColumns.has(alias)) return null;
275
+ return { type: 'Column', table: baseAlias, name: alias };
276
+ }
277
+
278
+ if ((term as any).type === 'AliasRef') {
279
+ const aliasName = (term as any).name;
280
+ if (!availableColumns.has(aliasName)) return null;
281
+ return { type: 'Column', table: baseAlias, name: aliasName };
282
+ }
283
+
284
+ return null;
285
+ }
286
+
274
287
  private buildPagingColumns(primaryKey: string, orderBy: OrderByNode[] | undefined, tableAlias: string): ColumnNode[] {
275
288
  const columns: ColumnNode[] = [{ type: 'Column', table: tableAlias, name: primaryKey, alias: primaryKey }];
276
289
 
277
290
  if (!orderBy) return columns;
278
291
 
279
292
  for (const ob of orderBy) {
280
- if (!columns.some(col => col.name === ob.column.name)) {
293
+ const term = ob.term as ColumnNode;
294
+ if (!columns.some(col => col.name === term.name)) {
281
295
  columns.push({
282
296
  type: 'Column',
283
297
  table: tableAlias,
284
- name: ob.column.name,
285
- alias: ob.column.name
298
+ name: term.name,
299
+ alias: term.name
286
300
  });
287
301
  }
288
302
  }
@@ -1,52 +1,61 @@
1
- import { TableDef } from '../schema/table.js';
2
- import { ColumnDef } from '../schema/column.js';
3
- import { SelectQueryNode, CommonTableExpressionNode, SetOperationKind, SetOperationNode, TableSourceNode } from '../core/ast/query.js';
4
- import { buildColumnNode } from '../core/ast/builders.js';
5
- import {
6
- ColumnNode,
7
- ExpressionNode,
8
- FunctionNode,
9
- CaseExpressionNode,
10
- WindowFunctionNode,
11
- ScalarSubqueryNode,
12
- and,
13
- isExpressionSelectionNode
14
- } from '../core/ast/expression.js';
15
- import { JoinNode } from '../core/ast/join.js';
16
- import { SelectQueryState, ProjectionNode } from './select-query-state.js';
17
- import { OrderDirection } from '../core/sql/sql.js';
18
- import { parseRawColumn } from './raw-column-parser.js';
19
-
20
- /**
21
- * Result of column selection operation
22
- */
23
- export interface ColumnSelectionResult {
24
- /**
25
- * Updated query state
26
- */
27
- state: SelectQueryState;
28
- /**
29
- * Columns that were added
30
- */
31
- addedColumns: ProjectionNode[];
32
- }
33
-
34
- /**
35
- * Service for manipulating query AST (Abstract Syntax Tree)
36
- */
37
- export class QueryAstService {
38
- /**
39
- * Creates a new QueryAstService instance
40
- * @param table - Table definition
41
- * @param state - Current query state
42
- */
43
- constructor(private readonly table: TableDef, private readonly state: SelectQueryState) {}
44
-
45
- /**
46
- * Selects columns for the query
47
- * @param columns - Columns to select (key: alias, value: column definition or expression)
48
- * @returns Column selection result with updated state and added columns
49
- */
1
+ import { TableDef } from '../schema/table.js';
2
+ import { ColumnDef } from '../schema/column.js';
3
+ import {
4
+ SelectQueryNode,
5
+ CommonTableExpressionNode,
6
+ SetOperationKind,
7
+ SetOperationNode,
8
+ TableSourceNode,
9
+ OrderingTerm
10
+ } from '../core/ast/query.js';
11
+ import { buildColumnNode } from '../core/ast/builders.js';
12
+ import {
13
+ AliasRefNode,
14
+ ColumnNode,
15
+ ExpressionNode,
16
+ FunctionNode,
17
+ CaseExpressionNode,
18
+ WindowFunctionNode,
19
+ ScalarSubqueryNode,
20
+ and,
21
+ isExpressionSelectionNode,
22
+ isOperandNode
23
+ } from '../core/ast/expression.js';
24
+ import { JoinNode } from '../core/ast/join.js';
25
+ import { SelectQueryState, ProjectionNode } from './select-query-state.js';
26
+ import { OrderDirection } from '../core/sql/sql.js';
27
+ import { parseRawColumn } from './raw-column-parser.js';
28
+
29
+ /**
30
+ * Result of column selection operation
31
+ */
32
+ export interface ColumnSelectionResult {
33
+ /**
34
+ * Updated query state
35
+ */
36
+ state: SelectQueryState;
37
+ /**
38
+ * Columns that were added
39
+ */
40
+ addedColumns: ProjectionNode[];
41
+ }
42
+
43
+ /**
44
+ * Service for manipulating query AST (Abstract Syntax Tree)
45
+ */
46
+ export class QueryAstService {
47
+ /**
48
+ * Creates a new QueryAstService instance
49
+ * @param table - Table definition
50
+ * @param state - Current query state
51
+ */
52
+ constructor(private readonly table: TableDef, private readonly state: SelectQueryState) {}
53
+
54
+ /**
55
+ * Selects columns for the query
56
+ * @param columns - Columns to select (key: alias, value: column definition or expression)
57
+ * @returns Column selection result with updated state and added columns
58
+ */
50
59
  select(
51
60
  columns: Record<string, ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode>
52
61
  ): ColumnSelectionResult {
@@ -77,16 +86,16 @@ export class QueryAstService {
77
86
  } as ColumnNode);
78
87
  return acc;
79
88
  }, []);
80
-
81
- const nextState = this.state.withColumns(newCols);
82
- return { state: nextState, addedColumns: newCols };
83
- }
84
-
85
- /**
86
- * Selects raw column expressions (best-effort parser for simple references/functions)
87
- * @param cols - Raw column expressions
88
- * @returns Column selection result with updated state and added columns
89
- */
89
+
90
+ const nextState = this.state.withColumns(newCols);
91
+ return { state: nextState, addedColumns: newCols };
92
+ }
93
+
94
+ /**
95
+ * Selects raw column expressions (best-effort parser for simple references/functions)
96
+ * @param cols - Raw column expressions
97
+ * @returns Column selection result with updated state and added columns
98
+ */
90
99
  selectRaw(cols: string[]): ColumnSelectionResult {
91
100
  const from = this.state.ast.from;
92
101
  const defaultTable = from.type === 'Table' && from.alias ? from.alias : this.table.name;
@@ -94,15 +103,15 @@ export class QueryAstService {
94
103
  const nextState = this.state.withColumns(newCols);
95
104
  return { state: nextState, addedColumns: newCols };
96
105
  }
97
-
98
- /**
99
- * Adds a Common Table Expression (CTE) to the query
100
- * @param name - Name of the CTE
101
- * @param query - Query for the CTE
102
- * @param columns - Optional column names for the CTE
103
- * @param recursive - Whether the CTE is recursive
104
- * @returns Updated query state with CTE
105
- */
106
+
107
+ /**
108
+ * Adds a Common Table Expression (CTE) to the query
109
+ * @param name - Name of the CTE
110
+ * @param query - Query for the CTE
111
+ * @param columns - Optional column names for the CTE
112
+ * @param recursive - Whether the CTE is recursive
113
+ * @returns Updated query state with CTE
114
+ */
106
115
  withCte(name: string, query: SelectQueryNode, columns?: string[], recursive = false): SelectQueryState {
107
116
  const cte: CommonTableExpressionNode = {
108
117
  type: 'CommonTableExpression',
@@ -138,107 +147,135 @@ export class QueryAstService {
138
147
  withFrom(from: TableSourceNode): SelectQueryState {
139
148
  return this.state.withFrom(from);
140
149
  }
141
-
142
- /**
143
- * Selects a subquery as a column
144
- * @param alias - Alias for the subquery
145
- * @param query - Subquery to select
146
- * @returns Updated query state with subquery selection
147
- */
148
- selectSubquery(alias: string, query: SelectQueryNode): SelectQueryState {
149
- const node: ScalarSubqueryNode = { type: 'ScalarSubquery', query, alias };
150
- return this.state.withColumns([node]);
151
- }
152
-
153
- /**
154
- * Adds a JOIN clause to the query
155
- * @param join - Join node to add
156
- * @returns Updated query state with JOIN
157
- */
158
- withJoin(join: JoinNode): SelectQueryState {
159
- return this.state.withJoin(join);
160
- }
161
-
162
- /**
163
- * Adds a WHERE clause to the query
164
- * @param expr - Expression for the WHERE clause
165
- * @returns Updated query state with WHERE clause
166
- */
167
- withWhere(expr: ExpressionNode): SelectQueryState {
168
- const combined = this.combineExpressions(this.state.ast.where, expr);
169
- return this.state.withWhere(combined);
170
- }
171
-
172
- /**
173
- * Adds a GROUP BY clause to the query
174
- * @param col - Column to group by
175
- * @returns Updated query state with GROUP BY clause
176
- */
177
- withGroupBy(col: ColumnDef | ColumnNode): SelectQueryState {
178
- const from = this.state.ast.from;
179
- const tableRef = from.type === 'Table' && from.alias ? { ...this.table, alias: from.alias } : this.table;
180
- const node = buildColumnNode(tableRef, col);
181
- return this.state.withGroupBy([node]);
182
- }
183
-
184
- /**
185
- * Adds a HAVING clause to the query
186
- * @param expr - Expression for the HAVING clause
187
- * @returns Updated query state with HAVING clause
188
- */
189
- withHaving(expr: ExpressionNode): SelectQueryState {
190
- const combined = this.combineExpressions(this.state.ast.having, expr);
191
- return this.state.withHaving(combined);
192
- }
193
-
194
- /**
195
- * Adds an ORDER BY clause to the query
196
- * @param col - Column to order by
197
- * @param direction - Order direction (ASC/DESC)
198
- * @returns Updated query state with ORDER BY clause
199
- */
200
- withOrderBy(col: ColumnDef | ColumnNode, direction: OrderDirection): SelectQueryState {
150
+
151
+ /**
152
+ * Selects a subquery as a column
153
+ * @param alias - Alias for the subquery
154
+ * @param query - Subquery to select
155
+ * @returns Updated query state with subquery selection
156
+ */
157
+ selectSubquery(alias: string, query: SelectQueryNode): SelectQueryState {
158
+ const node: ScalarSubqueryNode = { type: 'ScalarSubquery', query, alias };
159
+ return this.state.withColumns([node]);
160
+ }
161
+
162
+ /**
163
+ * Adds a JOIN clause to the query
164
+ * @param join - Join node to add
165
+ * @returns Updated query state with JOIN
166
+ */
167
+ withJoin(join: JoinNode): SelectQueryState {
168
+ return this.state.withJoin(join);
169
+ }
170
+
171
+ /**
172
+ * Adds a WHERE clause to the query
173
+ * @param expr - Expression for the WHERE clause
174
+ * @returns Updated query state with WHERE clause
175
+ */
176
+ withWhere(expr: ExpressionNode): SelectQueryState {
177
+ const combined = this.combineExpressions(this.state.ast.where, expr);
178
+ return this.state.withWhere(combined);
179
+ }
180
+
181
+ /**
182
+ * Adds a GROUP BY clause to the query
183
+ * @param col - Column to group by
184
+ * @returns Updated query state with GROUP BY clause
185
+ */
186
+ withGroupBy(col: ColumnDef | OrderingTerm): SelectQueryState {
187
+ const term = this.normalizeOrderingTerm(col);
188
+ return this.state.withGroupBy([term]);
189
+ }
190
+
191
+ /**
192
+ * Adds a HAVING clause to the query
193
+ * @param expr - Expression for the HAVING clause
194
+ * @returns Updated query state with HAVING clause
195
+ */
196
+ withHaving(expr: ExpressionNode): SelectQueryState {
197
+ const combined = this.combineExpressions(this.state.ast.having, expr);
198
+ return this.state.withHaving(combined);
199
+ }
200
+
201
+ /**
202
+ * Adds an ORDER BY clause to the query
203
+ * @param col - Column to order by
204
+ * @param direction - Order direction (ASC/DESC)
205
+ * @returns Updated query state with ORDER BY clause
206
+ */
207
+ withOrderBy(
208
+ term: ColumnDef | OrderingTerm,
209
+ direction: OrderDirection,
210
+ nulls?: 'FIRST' | 'LAST',
211
+ collation?: string
212
+ ): SelectQueryState {
213
+ const normalized = this.normalizeOrderingTerm(term);
214
+ return this.state.withOrderBy([{ type: 'OrderBy', term: normalized, direction, nulls, collation }]);
215
+ }
216
+
217
+ /**
218
+ * Adds a DISTINCT clause to the query
219
+ * @param cols - Columns to make distinct
220
+ * @returns Updated query state with DISTINCT clause
221
+ */
222
+ withDistinct(cols: ColumnNode[]): SelectQueryState {
223
+ return this.state.withDistinct(cols);
224
+ }
225
+
226
+ /**
227
+ * Adds a LIMIT clause to the query
228
+ * @param limit - Maximum number of rows to return
229
+ * @returns Updated query state with LIMIT clause
230
+ */
231
+ withLimit(limit: number): SelectQueryState {
232
+ return this.state.withLimit(limit);
233
+ }
234
+
235
+ /**
236
+ * Adds an OFFSET clause to the query
237
+ * @param offset - Number of rows to skip
238
+ * @returns Updated query state with OFFSET clause
239
+ */
240
+ withOffset(offset: number): SelectQueryState {
241
+ return this.state.withOffset(offset);
242
+ }
243
+
244
+ /**
245
+ * Combines expressions with AND operator
246
+ * @param existing - Existing expression
247
+ * @param next - New expression to combine
248
+ * @returns Combined expression
249
+ */
250
+ private combineExpressions(existing: ExpressionNode | undefined, next: ExpressionNode): ExpressionNode {
251
+ return existing ? and(existing, next) : next;
252
+ }
253
+
254
+ private normalizeOrderingTerm(term: ColumnDef | OrderingTerm): OrderingTerm {
201
255
  const from = this.state.ast.from;
202
256
  const tableRef = from.type === 'Table' && from.alias ? { ...this.table, alias: from.alias } : this.table;
203
- const node = buildColumnNode(tableRef, col);
204
- return this.state.withOrderBy([{ type: 'OrderBy', column: node, direction }]);
205
- }
206
-
207
- /**
208
- * Adds a DISTINCT clause to the query
209
- * @param cols - Columns to make distinct
210
- * @returns Updated query state with DISTINCT clause
211
- */
212
- withDistinct(cols: ColumnNode[]): SelectQueryState {
213
- return this.state.withDistinct(cols);
214
- }
215
-
216
- /**
217
- * Adds a LIMIT clause to the query
218
- * @param limit - Maximum number of rows to return
219
- * @returns Updated query state with LIMIT clause
220
- */
221
- withLimit(limit: number): SelectQueryState {
222
- return this.state.withLimit(limit);
223
- }
224
-
225
- /**
226
- * Adds an OFFSET clause to the query
227
- * @param offset - Number of rows to skip
228
- * @returns Updated query state with OFFSET clause
229
- */
230
- withOffset(offset: number): SelectQueryState {
231
- return this.state.withOffset(offset);
232
- }
233
-
234
- /**
235
- * Combines expressions with AND operator
236
- * @param existing - Existing expression
237
- * @param next - New expression to combine
238
- * @returns Combined expression
239
- */
240
- private combineExpressions(existing: ExpressionNode | undefined, next: ExpressionNode): ExpressionNode {
241
- return existing ? and(existing, next) : next;
242
- }
243
-
244
- }
257
+ const termType = (term as any)?.type;
258
+ if (termType === 'Column') {
259
+ return term as ColumnNode;
260
+ }
261
+ if (termType === 'AliasRef') {
262
+ return term as AliasRefNode;
263
+ }
264
+ if (isOperandNode(term)) {
265
+ return term as OrderingTerm;
266
+ }
267
+ if (
268
+ termType === 'BinaryExpression' ||
269
+ termType === 'LogicalExpression' ||
270
+ termType === 'NullExpression' ||
271
+ termType === 'InExpression' ||
272
+ termType === 'ExistsExpression' ||
273
+ termType === 'BetweenExpression' ||
274
+ termType === 'ArithmeticExpression'
275
+ ) {
276
+ return term as ExpressionNode;
277
+ }
278
+ return buildColumnNode(tableRef, term as ColumnDef);
279
+ }
280
+
281
+ }