metal-orm 1.0.58 → 1.0.59

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 (40) hide show
  1. package/README.md +34 -31
  2. package/dist/index.cjs +1463 -1003
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +148 -129
  5. package/dist/index.d.ts +148 -129
  6. package/dist/index.js +1459 -1003
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/core/ddl/schema-generator.ts +44 -1
  10. package/src/decorators/bootstrap.ts +183 -146
  11. package/src/decorators/column-decorator.ts +8 -49
  12. package/src/decorators/decorator-metadata.ts +10 -46
  13. package/src/decorators/entity.ts +30 -40
  14. package/src/decorators/relations.ts +30 -56
  15. package/src/orm/entity-hydration.ts +72 -0
  16. package/src/orm/entity-meta.ts +13 -11
  17. package/src/orm/entity-metadata.ts +240 -238
  18. package/src/orm/entity-relation-cache.ts +39 -0
  19. package/src/orm/entity-relations.ts +207 -0
  20. package/src/orm/entity.ts +124 -410
  21. package/src/orm/execute.ts +4 -4
  22. package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
  23. package/src/orm/lazy-batch/belongs-to.ts +108 -0
  24. package/src/orm/lazy-batch/has-many.ts +69 -0
  25. package/src/orm/lazy-batch/has-one.ts +68 -0
  26. package/src/orm/lazy-batch/shared.ts +125 -0
  27. package/src/orm/lazy-batch.ts +4 -492
  28. package/src/orm/relations/many-to-many.ts +2 -1
  29. package/src/query-builder/relation-cte-builder.ts +63 -0
  30. package/src/query-builder/relation-filter-utils.ts +159 -0
  31. package/src/query-builder/relation-include-strategies.ts +177 -0
  32. package/src/query-builder/relation-join-planner.ts +80 -0
  33. package/src/query-builder/relation-service.ts +119 -479
  34. package/src/query-builder/relation-types.ts +41 -10
  35. package/src/query-builder/select/projection-facet.ts +23 -23
  36. package/src/query-builder/select/select-operations.ts +145 -0
  37. package/src/query-builder/select.ts +351 -422
  38. package/src/schema/relation.ts +22 -18
  39. package/src/schema/table.ts +22 -9
  40. package/src/schema/types.ts +14 -12
@@ -1,57 +1,30 @@
1
1
  import { TableDef } from '../schema/table.js';
2
2
  import { ColumnDef } from '../schema/column-types.js';
3
- import {
4
- RelationDef,
5
- RelationKinds,
6
- BelongsToManyRelation,
7
- HasManyRelation,
8
- HasOneRelation,
9
- BelongsToRelation
10
- } from '../schema/relation.js';
11
- import { SelectQueryNode, TableSourceNode, TableNode, OrderingTerm } from '../core/ast/query.js';
12
- import {
13
- ColumnNode,
14
- ExpressionNode,
15
- OperandNode,
16
- and,
17
- isOperandNode
18
- } from '../core/ast/expression.js';
19
- import { SelectQueryState } from './select-query-state.js';
20
- import { HydrationManager } from './hydration-manager.js';
21
- import { QueryAstService } from './query-ast-service.js';
22
- import { findPrimaryKey } from './hydration-planner.js';
23
- import { RelationProjectionHelper } from './relation-projection-helper.js';
24
- import type { RelationResult } from './relation-projection-helper.js';
25
- import {
26
- buildRelationJoinCondition,
27
- buildRelationCorrelation,
28
- buildBelongsToManyJoins
29
- } from './relation-conditions.js';
30
- import { JoinKind, JOIN_KINDS } from '../core/sql/sql.js';
3
+ import { RelationDef } from '../schema/relation.js';
4
+ import { SelectQueryNode, TableSourceNode, TableNode } from '../core/ast/query.js';
5
+ import { ColumnNode, ExpressionNode, and } from '../core/ast/expression.js';
6
+ import { SelectQueryState } from './select-query-state.js';
7
+ import { HydrationManager } from './hydration-manager.js';
8
+ import { QueryAstService } from './query-ast-service.js';
9
+ import { findPrimaryKey } from './hydration-planner.js';
10
+ import { RelationProjectionHelper } from './relation-projection-helper.js';
11
+ import type { RelationResult } from './relation-projection-helper.js';
12
+ import { buildRelationCorrelation } from './relation-conditions.js';
13
+ import { JoinKind, JOIN_KINDS } from '../core/sql/sql.js';
31
14
  import { RelationIncludeOptions } from './relation-types.js';
32
- import { createJoinNode } from '../core/ast/join-node.js';
33
15
  import { getJoinRelationName } from '../core/ast/join-metadata.js';
34
- import { makeRelationAlias } from './relation-alias.js';
35
- import { buildDefaultPivotColumns } from './relation-utils.js';
36
-
37
- type FilterTableCollector = {
38
- tables: Set<string>;
39
- hasSubquery: boolean;
40
- };
41
-
42
- type RelationWithForeignKey =
43
- | HasManyRelation
44
- | HasOneRelation
45
- | BelongsToRelation;
46
-
47
- const hasRelationForeignKey = (relation: RelationDef): relation is RelationWithForeignKey =>
48
- relation.type !== RelationKinds.BelongsToMany;
49
-
50
- /**
51
- * Service for handling relation operations (joins, includes, etc.)
52
- */
16
+ import { splitFilterExpressions } from './relation-filter-utils.js';
17
+ import { RelationJoinPlanner } from './relation-join-planner.js';
18
+ import { RelationCteBuilder } from './relation-cte-builder.js';
19
+ import { relationIncludeStrategies } from './relation-include-strategies.js';
20
+
21
+ /**
22
+ * Service for handling relation operations (joins, includes, etc.)
23
+ */
53
24
  export class RelationService {
54
- private readonly projectionHelper: RelationProjectionHelper;
25
+ private readonly projectionHelper: RelationProjectionHelper;
26
+ private readonly joinPlanner: RelationJoinPlanner;
27
+ private readonly cteBuilder: RelationCteBuilder;
55
28
 
56
29
  /**
57
30
  * Creates a new RelationService instance
@@ -64,11 +37,13 @@ export class RelationService {
64
37
  private readonly state: SelectQueryState,
65
38
  private readonly hydration: HydrationManager,
66
39
  private readonly createQueryAstService: (table: TableDef, state: SelectQueryState) => QueryAstService
67
- ) {
68
- this.projectionHelper = new RelationProjectionHelper(table, (state, hydration, columns) =>
69
- this.selectColumns(state, hydration, columns)
70
- );
71
- }
40
+ ) {
41
+ this.projectionHelper = new RelationProjectionHelper(table, (state, hydration, columns) =>
42
+ this.selectColumns(state, hydration, columns)
43
+ );
44
+ this.joinPlanner = new RelationJoinPlanner(table, createQueryAstService);
45
+ this.cteBuilder = new RelationCteBuilder(table, createQueryAstService);
46
+ }
72
47
 
73
48
  /**
74
49
  * Joins a relation to the query
@@ -77,13 +52,21 @@ export class RelationService {
77
52
  * @param extraCondition - Additional join condition
78
53
  * @returns Relation result with updated state and hydration
79
54
  */
80
- joinRelation(
81
- relationName: string,
82
- joinKind: JoinKind,
83
- extraCondition?: ExpressionNode,
84
- tableSource?: TableSourceNode
55
+ joinRelation(
56
+ relationName: string,
57
+ joinKind: JoinKind,
58
+ extraCondition?: ExpressionNode,
59
+ tableSource?: TableSourceNode
85
60
  ): RelationResult {
86
- const nextState = this.withJoin(this.state, relationName, joinKind, extraCondition, tableSource);
61
+ const relation = this.getRelation(relationName);
62
+ const nextState = this.joinPlanner.withJoin(
63
+ this.state,
64
+ relationName,
65
+ relation,
66
+ joinKind,
67
+ extraCondition,
68
+ tableSource
69
+ );
87
70
  return { state: nextState, hydration: this.hydration };
88
71
  }
89
72
 
@@ -111,148 +94,67 @@ export class RelationService {
111
94
  * @param options - Options for relation inclusion
112
95
  * @returns Relation result with updated state and hydration
113
96
  */
114
- include(relationName: string, options?: RelationIncludeOptions): RelationResult {
115
- let state = this.state;
116
- let hydration = this.hydration;
117
-
118
- const relation = this.getRelation(relationName);
119
- const aliasPrefix = options?.aliasPrefix ?? relationName;
120
- const alreadyJoined = state.ast.joins.some(j => getJoinRelationName(j) === relationName);
121
- const { selfFilters, crossFilters } = this.splitFilterExpressions(
97
+ include(relationName: string, options?: RelationIncludeOptions): RelationResult {
98
+ let state = this.state;
99
+ let hydration = this.hydration;
100
+
101
+ const relation = this.getRelation(relationName);
102
+ const aliasPrefix = options?.aliasPrefix ?? relationName;
103
+ const alreadyJoined = state.ast.joins.some(j => getJoinRelationName(j) === relationName);
104
+ const { selfFilters, crossFilters } = splitFilterExpressions(
122
105
  options?.filter,
123
106
  new Set([relation.target.name])
124
107
  );
125
- const canUseCte = !alreadyJoined && selfFilters.length > 0;
126
- const joinFilters = [...crossFilters];
127
- if (!canUseCte) {
128
- joinFilters.push(...selfFilters);
129
- }
130
- const joinCondition = this.combineWithAnd(joinFilters);
131
-
132
- let tableSourceOverride: TableNode | undefined;
108
+ const canUseCte = !alreadyJoined && selfFilters.length > 0;
109
+ const joinFilters = [...crossFilters];
110
+ if (!canUseCte) {
111
+ joinFilters.push(...selfFilters);
112
+ }
113
+ const joinCondition = this.combineWithAnd(joinFilters);
114
+
115
+ let tableSourceOverride: TableNode | undefined;
133
116
  if (canUseCte) {
134
- const cteInfo = this.createFilteredRelationCte(state, relationName, relation, selfFilters);
117
+ const predicate = this.combineWithAnd(selfFilters);
118
+ const cteInfo = this.cteBuilder.createFilteredRelationCte(
119
+ state,
120
+ relationName,
121
+ relation,
122
+ predicate
123
+ );
135
124
  state = cteInfo.state;
136
125
  tableSourceOverride = cteInfo.table;
137
126
  }
138
127
 
139
128
  if (!alreadyJoined) {
140
- state = this.withJoin(
129
+ state = this.joinPlanner.withJoin(
141
130
  state,
142
131
  relationName,
132
+ relation,
143
133
  options?.joinKind ?? JOIN_KINDS.LEFT,
144
134
  joinCondition,
145
135
  tableSourceOverride
146
136
  );
147
137
  }
148
138
 
149
- const projectionResult = this.projectionHelper.ensureBaseProjection(state, hydration);
150
- state = projectionResult.state;
151
- hydration = projectionResult.hydration;
152
-
153
- if (hasRelationForeignKey(relation)) {
154
- const fkColumn = this.table.columns[relation.foreignKey];
155
- if (fkColumn) {
156
- const hasForeignKeySelected = state.ast.columns.some(col => {
157
- if ((col as ColumnNode).type !== 'Column') return false;
158
- const node = col as ColumnNode;
159
- const alias = node.alias ?? node.name;
160
- return alias === relation.foreignKey;
161
- });
162
-
163
- if (!hasForeignKeySelected) {
164
- const fkSelectionResult = this.selectColumns(state, hydration, {
165
- [relation.foreignKey]: fkColumn
166
- });
167
- state = fkSelectionResult.state;
168
- hydration = fkSelectionResult.hydration;
169
- }
170
- }
171
- }
172
-
173
- const requestedColumns = options?.columns?.length
174
- ? [...options.columns]
175
- : Object.keys(relation.target.columns);
176
- const targetPrimaryKey = findPrimaryKey(relation.target);
177
- if (!requestedColumns.includes(targetPrimaryKey)) {
178
- requestedColumns.push(targetPrimaryKey);
179
- }
180
- const targetColumns = requestedColumns;
181
-
182
- const buildTypedSelection = (
183
- columns: Record<string, ColumnDef>,
184
- prefix: string,
185
- keys: string[],
186
- missingMsg: (col: string) => string
187
- ): Record<string, ColumnDef> => {
188
- return keys.reduce((acc, key) => {
189
- const def = columns[key];
190
- if (!def) {
191
- throw new Error(missingMsg(key));
192
- }
193
- acc[makeRelationAlias(prefix, key)] = def;
194
- return acc;
195
- }, {} as Record<string, ColumnDef>);
196
- };
197
-
198
- const targetSelection = buildTypedSelection(
199
- relation.target.columns as Record<string, ColumnDef>,
200
- aliasPrefix,
201
- targetColumns,
202
- key => `Column '${key}' not found on relation '${relationName}'`
203
- );
204
-
205
- if (relation.type !== RelationKinds.BelongsToMany) {
206
- const relationSelectionResult = this.selectColumns(state, hydration, targetSelection);
207
- state = relationSelectionResult.state;
208
- hydration = relationSelectionResult.hydration;
209
-
210
- hydration = hydration.onRelationIncluded(
211
- state,
212
- relation,
213
- relationName,
214
- aliasPrefix,
215
- targetColumns
216
- );
217
-
218
- return { state, hydration };
219
- }
220
-
221
- const many = relation as BelongsToManyRelation;
222
- const pivotAliasPrefix = options?.pivot?.aliasPrefix ?? `${aliasPrefix}_pivot`;
223
- const pivotPk = many.pivotPrimaryKey || findPrimaryKey(many.pivotTable);
224
- const pivotColumns =
225
- options?.pivot?.columns ??
226
- many.defaultPivotColumns ??
227
- buildDefaultPivotColumns(many, pivotPk);
139
+ const projectionResult = this.projectionHelper.ensureBaseProjection(state, hydration);
140
+ state = projectionResult.state;
141
+ hydration = projectionResult.hydration;
228
142
 
229
- const pivotSelection = buildTypedSelection(
230
- many.pivotTable.columns as Record<string, ColumnDef>,
231
- pivotAliasPrefix,
232
- pivotColumns,
233
- key => `Column '${key}' not found on pivot table '${many.pivotTable.name}'`
234
- );
235
-
236
- const combinedSelection = {
237
- ...targetSelection,
238
- ...pivotSelection
239
- };
240
-
241
- const relationSelectionResult = this.selectColumns(state, hydration, combinedSelection);
242
- state = relationSelectionResult.state;
243
- hydration = relationSelectionResult.hydration;
244
-
245
- hydration = hydration.onRelationIncluded(
246
- state,
247
- relation,
248
- relationName,
249
- aliasPrefix,
250
- targetColumns,
251
- { aliasPrefix: pivotAliasPrefix, columns: pivotColumns }
252
- );
253
-
254
- return { state, hydration };
255
- }
143
+ const strategy = relationIncludeStrategies[relation.type];
144
+ const result = strategy({
145
+ rootTable: this.table,
146
+ state,
147
+ hydration,
148
+ relation,
149
+ relationName,
150
+ aliasPrefix,
151
+ options,
152
+ selectColumns: (nextState, nextHydration, columns) =>
153
+ this.selectColumns(nextState, nextHydration, columns)
154
+ });
155
+
156
+ return { state: result.state, hydration: result.hydration };
157
+ }
256
158
 
257
159
  /**
258
160
  * Applies relation correlation to a query AST
@@ -281,298 +183,36 @@ export class RelationService {
281
183
  };
282
184
  }
283
185
 
284
- /**
285
- * Creates a join node for a relation
286
- * @param state - Current query state
287
- * @param relationName - Name of the relation
288
- * @param joinKind - Type of join to use
289
- * @param extraCondition - Additional join condition
290
- * @returns Updated query state with join
291
- */
292
- private withJoin(
293
- state: SelectQueryState,
294
- relationName: string,
295
- joinKind: JoinKind,
296
- extraCondition?: ExpressionNode,
297
- tableSource?: TableSourceNode
298
- ): SelectQueryState {
299
- const relation = this.getRelation(relationName);
300
- const rootAlias = state.ast.from.type === 'Table' ? state.ast.from.alias : undefined;
301
- if (relation.type === RelationKinds.BelongsToMany) {
302
- const targetTableSource: TableSourceNode = tableSource ?? {
303
- type: 'Table',
304
- name: relation.target.name,
305
- schema: relation.target.schema
306
- };
307
- const targetName = this.resolveTargetTableName(targetTableSource, relation);
308
- const joins = buildBelongsToManyJoins(
309
- this.table,
310
- relationName,
311
- relation as BelongsToManyRelation,
312
- joinKind,
313
- extraCondition,
314
- rootAlias,
315
- targetTableSource,
316
- targetName
317
- );
318
- return joins.reduce((current, join) => this.astService(current).withJoin(join), state);
319
- }
320
-
321
- const targetTable: TableSourceNode = tableSource ?? {
322
- type: 'Table',
323
- name: relation.target.name,
324
- schema: relation.target.schema
325
- };
326
- const targetName = this.resolveTargetTableName(targetTable, relation);
327
- const condition = buildRelationJoinCondition(
328
- this.table,
329
- relation,
330
- extraCondition,
331
- rootAlias,
332
- targetName
333
- );
334
- const joinNode = createJoinNode(joinKind, targetTable, condition, relationName);
335
-
336
- return this.astService(state).withJoin(joinNode);
337
- }
338
-
339
- /**
340
- * Selects columns for a relation
186
+ /**
187
+ * Selects columns for a relation
341
188
  * @param state - Current query state
342
189
  * @param hydration - Hydration manager
343
190
  * @param columns - Columns to select
344
191
  * @returns Relation result with updated state and hydration
345
192
  */
346
- private selectColumns(
347
- state: SelectQueryState,
348
- hydration: HydrationManager,
349
- columns: Record<string, ColumnDef>
350
- ): RelationResult {
351
- const { state: nextState, addedColumns } = this.astService(state).select(columns);
352
- return {
353
- state: nextState,
354
- hydration: hydration.onColumnsSelected(nextState, addedColumns)
355
- };
356
- }
357
-
358
-
359
- private combineWithAnd(expressions: ExpressionNode[]): ExpressionNode | undefined {
360
- if (expressions.length === 0) return undefined;
361
- if (expressions.length === 1) return expressions[0];
362
- return {
363
- type: 'LogicalExpression',
364
- operator: 'AND',
365
- operands: expressions
366
- };
367
- }
368
-
369
- private splitFilterExpressions(
370
- filter: ExpressionNode | undefined,
371
- allowedTables: Set<string>
372
- ): { selfFilters: ExpressionNode[]; crossFilters: ExpressionNode[] } {
373
- const terms = this.flattenAnd(filter);
374
- const selfFilters: ExpressionNode[] = [];
375
- const crossFilters: ExpressionNode[] = [];
376
-
377
- for (const term of terms) {
378
- if (this.isExpressionSelfContained(term, allowedTables)) {
379
- selfFilters.push(term);
380
- } else {
381
- crossFilters.push(term);
382
- }
383
- }
384
-
385
- return { selfFilters, crossFilters };
386
- }
387
-
388
- private flattenAnd(node?: ExpressionNode): ExpressionNode[] {
389
- if (!node) return [];
390
- if (node.type === 'LogicalExpression' && node.operator === 'AND') {
391
- return node.operands.flatMap(operand => this.flattenAnd(operand));
392
- }
393
- return [node];
394
- }
395
-
396
- private isExpressionSelfContained(expr: ExpressionNode, allowedTables: Set<string>): boolean {
397
- const collector = this.collectReferencedTables(expr);
398
- if (collector.hasSubquery) return false;
399
- if (collector.tables.size === 0) return true;
400
- for (const table of collector.tables) {
401
- if (!allowedTables.has(table)) {
402
- return false;
403
- }
404
- }
405
- return true;
406
- }
407
-
408
- private collectReferencedTables(expr: ExpressionNode): FilterTableCollector {
409
- const collector: FilterTableCollector = {
410
- tables: new Set(),
411
- hasSubquery: false
412
- };
413
- this.collectFromExpression(expr, collector);
414
- return collector;
415
- }
416
-
417
- private collectFromExpression(expr: ExpressionNode, collector: FilterTableCollector): void {
418
- switch (expr.type) {
419
- case 'BinaryExpression':
420
- this.collectFromOperand(expr.left, collector);
421
- this.collectFromOperand(expr.right, collector);
422
- break;
423
- case 'LogicalExpression':
424
- expr.operands.forEach(operand => this.collectFromExpression(operand, collector));
425
- break;
426
- case 'NullExpression':
427
- this.collectFromOperand(expr.left, collector);
428
- break;
429
- case 'InExpression':
430
- this.collectFromOperand(expr.left, collector);
431
- if (Array.isArray(expr.right)) {
432
- expr.right.forEach(value => this.collectFromOperand(value, collector));
433
- } else {
434
- collector.hasSubquery = true;
435
- }
436
- break;
437
- case 'ExistsExpression':
438
- collector.hasSubquery = true;
439
- break;
440
- case 'BetweenExpression':
441
- this.collectFromOperand(expr.left, collector);
442
- this.collectFromOperand(expr.lower, collector);
443
- this.collectFromOperand(expr.upper, collector);
444
- break;
445
- case 'ArithmeticExpression':
446
- case 'BitwiseExpression':
447
- this.collectFromOperand(expr.left, collector);
448
- this.collectFromOperand(expr.right, collector);
449
- break;
450
- default:
451
- break;
452
- }
453
- }
454
-
455
- private collectFromOperand(node: OperandNode, collector: FilterTableCollector): void {
456
- switch (node.type) {
457
- case 'Column':
458
- collector.tables.add(node.table);
459
- break;
460
- case 'Function':
461
- node.args.forEach(arg => this.collectFromOperand(arg, collector));
462
- if (node.separator) {
463
- this.collectFromOperand(node.separator, collector);
464
- }
465
- if (node.orderBy) {
466
- node.orderBy.forEach(order => this.collectFromOrderingTerm(order.term, collector));
467
- }
468
- break;
469
- case 'JsonPath':
470
- this.collectFromOperand(node.column, collector);
471
- break;
472
- case 'ScalarSubquery':
473
- collector.hasSubquery = true;
474
- break;
475
- case 'CaseExpression':
476
- node.conditions.forEach(({ when, then }) => {
477
- this.collectFromExpression(when, collector);
478
- this.collectFromOperand(then, collector);
479
- });
480
- if (node.else) {
481
- this.collectFromOperand(node.else, collector);
482
- }
483
- break;
484
- case 'Cast':
485
- this.collectFromOperand(node.expression, collector);
486
- break;
487
- case 'WindowFunction':
488
- node.args.forEach(arg => this.collectFromOperand(arg, collector));
489
- node.partitionBy?.forEach(part => this.collectFromOperand(part, collector));
490
- node.orderBy?.forEach(order => this.collectFromOrderingTerm(order.term, collector));
491
- break;
492
- case 'Collate':
493
- this.collectFromOperand(node.expression, collector);
494
- break;
495
- case 'ArithmeticExpression':
496
- case 'BitwiseExpression':
497
- this.collectFromOperand(node.left, collector);
498
- this.collectFromOperand(node.right, collector);
499
- break;
500
- case 'Literal':
501
- case 'AliasRef':
502
- break;
503
- default:
504
- break;
505
- }
506
- }
507
-
508
- private collectFromOrderingTerm(term: OrderingTerm, collector: FilterTableCollector): void {
509
- if (isOperandNode(term)) {
510
- this.collectFromOperand(term, collector);
511
- return;
512
- }
513
- this.collectFromExpression(term, collector);
514
- }
515
-
516
- private createFilteredRelationCte(
517
- state: SelectQueryState,
518
- relationName: string,
519
- relation: RelationDef,
520
- filters: ExpressionNode[]
521
- ): { state: SelectQueryState; table: TableNode } {
522
- const cteName = this.generateUniqueCteName(state, relationName);
523
- const predicate = this.combineWithAnd(filters);
524
- if (!predicate) {
525
- throw new Error('Unable to build filter CTE without predicates.');
526
- }
527
-
528
- const columns: ColumnNode[] = Object.keys(relation.target.columns).map(name => ({
529
- type: 'Column',
530
- table: relation.target.name,
531
- name
532
- }));
533
-
534
- const cteQuery: SelectQueryNode = {
535
- type: 'SelectQuery',
536
- from: { type: 'Table', name: relation.target.name, schema: relation.target.schema },
537
- columns,
538
- joins: [],
539
- where: predicate
540
- };
541
-
542
- const nextState = this.astService(state).withCte(cteName, cteQuery);
543
- const tableNode: TableNode = {
544
- type: 'Table',
545
- name: cteName,
546
- alias: relation.target.name
547
- };
548
-
549
- return { state: nextState, table: tableNode };
550
- }
551
-
552
- private generateUniqueCteName(state: SelectQueryState, relationName: string): string {
553
- const existing = new Set((state.ast.ctes ?? []).map(cte => cte.name));
554
- let candidate = `${relationName}__filtered`;
555
- let suffix = 1;
556
- while (existing.has(candidate)) {
557
- candidate = `${relationName}__filtered_${suffix}`;
558
- suffix += 1;
559
- }
560
- return candidate;
561
- }
562
-
563
- private resolveTargetTableName(target: TableSourceNode, relation: RelationDef): string {
564
- if (target.type === 'Table') {
565
- return target.alias ?? target.name;
566
- }
567
- if (target.type === 'DerivedTable') {
568
- return target.alias;
569
- }
570
- if (target.type === 'FunctionTable') {
571
- return target.alias ?? relation.target.name;
572
- }
573
- return relation.target.name;
574
- }
575
-
193
+ private selectColumns(
194
+ state: SelectQueryState,
195
+ hydration: HydrationManager,
196
+ columns: Record<string, ColumnDef>
197
+ ): RelationResult {
198
+ const { state: nextState, addedColumns } = this.astService(state).select(columns);
199
+ return {
200
+ state: nextState,
201
+ hydration: hydration.onColumnsSelected(nextState, addedColumns)
202
+ };
203
+ }
204
+
205
+
206
+ private combineWithAnd(expressions: ExpressionNode[]): ExpressionNode | undefined {
207
+ if (expressions.length === 0) return undefined;
208
+ if (expressions.length === 1) return expressions[0];
209
+ return {
210
+ type: 'LogicalExpression',
211
+ operator: 'AND',
212
+ operands: expressions
213
+ };
214
+ }
215
+
576
216
  /**
577
217
  * Gets a relation definition by name
578
218
  * @param relationName - Name of the relation
@@ -597,12 +237,12 @@ export class RelationService {
597
237
  return this.createQueryAstService(this.table, state);
598
238
  }
599
239
 
600
- private rootTableName(): string {
601
- const from = this.state.ast.from;
602
- if (from.type === 'Table' && from.alias) return from.alias;
603
- return this.table.name;
604
- }
605
- }
606
-
607
- export type { RelationResult } from './relation-projection-helper.js';
240
+ private rootTableName(): string {
241
+ const from = this.state.ast.from;
242
+ if (from.type === 'Table' && from.alias) return from.alias;
243
+ return this.table.name;
244
+ }
245
+ }
246
+
247
+ export type { RelationResult } from './relation-projection-helper.js';
608
248