metal-orm 1.0.2 → 1.0.4
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 +20 -0
- package/package.json +1 -1
- package/src/ast/expression.ts +433 -175
- package/src/ast/join.ts +8 -1
- package/src/ast/query.ts +64 -9
- package/src/builder/hydration-manager.ts +42 -11
- package/src/builder/hydration-planner.ts +80 -31
- package/src/builder/operations/column-selector.ts +37 -1
- package/src/builder/operations/cte-manager.ts +16 -0
- package/src/builder/operations/filter-manager.ts +32 -0
- package/src/builder/operations/join-manager.ts +17 -7
- package/src/builder/operations/pagination-manager.ts +19 -0
- package/src/builder/operations/relation-manager.ts +58 -3
- package/src/builder/query-ast-service.ts +100 -29
- package/src/builder/relation-conditions.ts +30 -1
- package/src/builder/relation-projection-helper.ts +43 -1
- package/src/builder/relation-service.ts +68 -13
- package/src/builder/relation-types.ts +6 -0
- package/src/builder/select-query-builder-deps.ts +64 -3
- package/src/builder/select-query-state.ts +72 -0
- package/src/builder/select.ts +166 -0
- package/src/codegen/typescript.ts +142 -44
- package/src/constants/sql-operator-config.ts +36 -0
- package/src/constants/sql.ts +125 -58
- package/src/dialect/abstract.ts +97 -22
- package/src/dialect/mssql/index.ts +27 -0
- package/src/dialect/mysql/index.ts +22 -0
- package/src/dialect/postgres/index.ts +22 -0
- package/src/dialect/sqlite/index.ts +22 -0
- package/src/runtime/als.ts +15 -1
- package/src/runtime/hydration.ts +20 -15
- package/src/schema/column.ts +45 -5
- package/src/schema/relation.ts +49 -2
- package/src/schema/table.ts +27 -3
- package/src/utils/join-node.ts +20 -0
- package/src/utils/raw-column-parser.ts +32 -0
- package/src/utils/relation-alias.ts +43 -0
|
@@ -1,25 +1,61 @@
|
|
|
1
1
|
import { ExpressionNode } from '../../ast/expression';
|
|
2
2
|
import { SelectQueryNode } from '../../ast/query';
|
|
3
3
|
import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps';
|
|
4
|
-
import { JoinKind
|
|
5
|
-
|
|
6
|
-
type RelationIncludeJoinKind = typeof JOIN_KINDS.LEFT | typeof JOIN_KINDS.INNER;
|
|
4
|
+
import { JoinKind } from '../../constants/sql';
|
|
5
|
+
import { RelationIncludeJoinKind } from '../relation-types';
|
|
7
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Options for including relations in queries
|
|
9
|
+
*/
|
|
8
10
|
export interface RelationIncludeOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Columns to include from the related table
|
|
13
|
+
*/
|
|
9
14
|
columns?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Alias prefix for the relation columns
|
|
17
|
+
*/
|
|
10
18
|
aliasPrefix?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Filter expression to apply to the relation
|
|
21
|
+
*/
|
|
11
22
|
filter?: ExpressionNode;
|
|
23
|
+
/**
|
|
24
|
+
* Type of join to use for the relation
|
|
25
|
+
*/
|
|
12
26
|
joinKind?: RelationIncludeJoinKind;
|
|
13
27
|
}
|
|
14
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Manages relation operations (joins, includes, etc.) for query building
|
|
31
|
+
*/
|
|
15
32
|
export class RelationManager {
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new RelationManager instance
|
|
35
|
+
* @param env - Query builder environment
|
|
36
|
+
*/
|
|
16
37
|
constructor(private readonly env: SelectQueryBuilderEnvironment) {}
|
|
17
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Matches records based on a relation with an optional predicate
|
|
41
|
+
* @param context - Current query context
|
|
42
|
+
* @param relationName - Name of the relation to match
|
|
43
|
+
* @param predicate - Optional predicate expression
|
|
44
|
+
* @returns Updated query context with relation match
|
|
45
|
+
*/
|
|
18
46
|
match(context: SelectQueryBuilderContext, relationName: string, predicate?: ExpressionNode): SelectQueryBuilderContext {
|
|
19
47
|
const result = this.createService(context).match(relationName, predicate);
|
|
20
48
|
return { state: result.state, hydration: result.hydration };
|
|
21
49
|
}
|
|
22
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Joins a relation to the query
|
|
53
|
+
* @param context - Current query context
|
|
54
|
+
* @param relationName - Name of the relation to join
|
|
55
|
+
* @param joinKind - Type of join to use
|
|
56
|
+
* @param extraCondition - Additional join condition
|
|
57
|
+
* @returns Updated query context with relation join
|
|
58
|
+
*/
|
|
23
59
|
joinRelation(
|
|
24
60
|
context: SelectQueryBuilderContext,
|
|
25
61
|
relationName: string,
|
|
@@ -30,6 +66,13 @@ export class RelationManager {
|
|
|
30
66
|
return { state: result.state, hydration: result.hydration };
|
|
31
67
|
}
|
|
32
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Includes a relation in the query result
|
|
71
|
+
* @param context - Current query context
|
|
72
|
+
* @param relationName - Name of the relation to include
|
|
73
|
+
* @param options - Options for relation inclusion
|
|
74
|
+
* @returns Updated query context with included relation
|
|
75
|
+
*/
|
|
33
76
|
include(
|
|
34
77
|
context: SelectQueryBuilderContext,
|
|
35
78
|
relationName: string,
|
|
@@ -39,10 +82,22 @@ export class RelationManager {
|
|
|
39
82
|
return { state: result.state, hydration: result.hydration };
|
|
40
83
|
}
|
|
41
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Applies relation correlation to a query AST
|
|
87
|
+
* @param context - Current query context
|
|
88
|
+
* @param relationName - Name of the relation
|
|
89
|
+
* @param ast - Query AST to modify
|
|
90
|
+
* @returns Modified query AST with relation correlation
|
|
91
|
+
*/
|
|
42
92
|
applyRelationCorrelation(context: SelectQueryBuilderContext, relationName: string, ast: SelectQueryNode): SelectQueryNode {
|
|
43
93
|
return this.createService(context).applyRelationCorrelation(relationName, ast);
|
|
44
94
|
}
|
|
45
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Creates a relation service instance
|
|
98
|
+
* @param context - Current query context
|
|
99
|
+
* @returns Relation service instance
|
|
100
|
+
*/
|
|
46
101
|
private createService(context: SelectQueryBuilderContext) {
|
|
47
102
|
return this.env.deps.createRelationService(this.env.table, context.state, context.hydration);
|
|
48
103
|
}
|
|
@@ -8,12 +8,20 @@ import {
|
|
|
8
8
|
CaseExpressionNode,
|
|
9
9
|
WindowFunctionNode,
|
|
10
10
|
ScalarSubqueryNode,
|
|
11
|
-
and
|
|
11
|
+
and,
|
|
12
|
+
isExpressionSelectionNode
|
|
12
13
|
} from '../ast/expression';
|
|
13
14
|
import { JoinNode } from '../ast/join';
|
|
14
15
|
import { SelectQueryState, ProjectionNode } from './select-query-state';
|
|
15
16
|
import { OrderDirection } from '../constants/sql';
|
|
16
|
-
|
|
17
|
+
import { parseRawColumn } from '../utils/raw-column-parser';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds a column node from a column definition or existing column node
|
|
21
|
+
* @param table - Table definition
|
|
22
|
+
* @param col - Column definition or column node
|
|
23
|
+
* @returns Column node
|
|
24
|
+
*/
|
|
17
25
|
export const buildColumnNode = (table: TableDef, col: ColumnDef | ColumnNode): ColumnNode => {
|
|
18
26
|
if ((col as ColumnNode).type === 'Column') {
|
|
19
27
|
return col as ColumnNode;
|
|
@@ -27,14 +35,36 @@ export const buildColumnNode = (table: TableDef, col: ColumnDef | ColumnNode): C
|
|
|
27
35
|
};
|
|
28
36
|
};
|
|
29
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Result of column selection operation
|
|
40
|
+
*/
|
|
30
41
|
export interface ColumnSelectionResult {
|
|
42
|
+
/**
|
|
43
|
+
* Updated query state
|
|
44
|
+
*/
|
|
31
45
|
state: SelectQueryState;
|
|
46
|
+
/**
|
|
47
|
+
* Columns that were added
|
|
48
|
+
*/
|
|
32
49
|
addedColumns: ProjectionNode[];
|
|
33
50
|
}
|
|
34
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Service for manipulating query AST (Abstract Syntax Tree)
|
|
54
|
+
*/
|
|
35
55
|
export class QueryAstService {
|
|
56
|
+
/**
|
|
57
|
+
* Creates a new QueryAstService instance
|
|
58
|
+
* @param table - Table definition
|
|
59
|
+
* @param state - Current query state
|
|
60
|
+
*/
|
|
36
61
|
constructor(private readonly table: TableDef, private readonly state: SelectQueryState) {}
|
|
37
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Selects columns for the query
|
|
65
|
+
* @param columns - Columns to select (key: alias, value: column definition or expression)
|
|
66
|
+
* @returns Column selection result with updated state and added columns
|
|
67
|
+
*/
|
|
38
68
|
select(
|
|
39
69
|
columns: Record<string, ColumnDef | FunctionNode | CaseExpressionNode | WindowFunctionNode>
|
|
40
70
|
): ColumnSelectionResult {
|
|
@@ -45,11 +75,7 @@ export class QueryAstService {
|
|
|
45
75
|
const newCols = Object.entries(columns).reduce<ProjectionNode[]>((acc, [alias, val]) => {
|
|
46
76
|
if (existingAliases.has(alias)) return acc;
|
|
47
77
|
|
|
48
|
-
if (
|
|
49
|
-
(val as any).type === 'Function' ||
|
|
50
|
-
(val as any).type === 'CaseExpression' ||
|
|
51
|
-
(val as any).type === 'WindowFunction'
|
|
52
|
-
) {
|
|
78
|
+
if (isExpressionSelectionNode(val)) {
|
|
53
79
|
acc.push({ ...(val as FunctionNode | CaseExpressionNode | WindowFunctionNode), alias } as ProjectionNode);
|
|
54
80
|
return acc;
|
|
55
81
|
}
|
|
@@ -68,12 +94,25 @@ export class QueryAstService {
|
|
|
68
94
|
return { state: nextState, addedColumns: newCols };
|
|
69
95
|
}
|
|
70
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Selects raw column expressions (best-effort parser for simple references/functions)
|
|
99
|
+
* @param cols - Raw column expressions
|
|
100
|
+
* @returns Column selection result with updated state and added columns
|
|
101
|
+
*/
|
|
71
102
|
selectRaw(cols: string[]): ColumnSelectionResult {
|
|
72
|
-
const newCols = cols.map(
|
|
103
|
+
const newCols = cols.map(col => parseRawColumn(col, this.table.name, this.state.ast.ctes));
|
|
73
104
|
const nextState = this.state.withColumns(newCols);
|
|
74
105
|
return { state: nextState, addedColumns: newCols };
|
|
75
106
|
}
|
|
76
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Adds a Common Table Expression (CTE) to the query
|
|
110
|
+
* @param name - Name of the CTE
|
|
111
|
+
* @param query - Query for the CTE
|
|
112
|
+
* @param columns - Optional column names for the CTE
|
|
113
|
+
* @param recursive - Whether the CTE is recursive
|
|
114
|
+
* @returns Updated query state with CTE
|
|
115
|
+
*/
|
|
77
116
|
withCte(name: string, query: SelectQueryNode, columns?: string[], recursive = false): SelectQueryState {
|
|
78
117
|
const cte: CommonTableExpressionNode = {
|
|
79
118
|
type: 'CommonTableExpression',
|
|
@@ -86,70 +125,102 @@ export class QueryAstService {
|
|
|
86
125
|
return this.state.withCte(cte);
|
|
87
126
|
}
|
|
88
127
|
|
|
128
|
+
/**
|
|
129
|
+
* Selects a subquery as a column
|
|
130
|
+
* @param alias - Alias for the subquery
|
|
131
|
+
* @param query - Subquery to select
|
|
132
|
+
* @returns Updated query state with subquery selection
|
|
133
|
+
*/
|
|
89
134
|
selectSubquery(alias: string, query: SelectQueryNode): SelectQueryState {
|
|
90
135
|
const node: ScalarSubqueryNode = { type: 'ScalarSubquery', query, alias };
|
|
91
136
|
return this.state.withColumns([node]);
|
|
92
137
|
}
|
|
93
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Adds a JOIN clause to the query
|
|
141
|
+
* @param join - Join node to add
|
|
142
|
+
* @returns Updated query state with JOIN
|
|
143
|
+
*/
|
|
94
144
|
withJoin(join: JoinNode): SelectQueryState {
|
|
95
145
|
return this.state.withJoin(join);
|
|
96
146
|
}
|
|
97
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Adds a WHERE clause to the query
|
|
150
|
+
* @param expr - Expression for the WHERE clause
|
|
151
|
+
* @returns Updated query state with WHERE clause
|
|
152
|
+
*/
|
|
98
153
|
withWhere(expr: ExpressionNode): SelectQueryState {
|
|
99
154
|
const combined = this.combineExpressions(this.state.ast.where, expr);
|
|
100
155
|
return this.state.withWhere(combined);
|
|
101
156
|
}
|
|
102
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Adds a GROUP BY clause to the query
|
|
160
|
+
* @param col - Column to group by
|
|
161
|
+
* @returns Updated query state with GROUP BY clause
|
|
162
|
+
*/
|
|
103
163
|
withGroupBy(col: ColumnDef | ColumnNode): SelectQueryState {
|
|
104
164
|
const node = buildColumnNode(this.table, col);
|
|
105
165
|
return this.state.withGroupBy([node]);
|
|
106
166
|
}
|
|
107
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Adds a HAVING clause to the query
|
|
170
|
+
* @param expr - Expression for the HAVING clause
|
|
171
|
+
* @returns Updated query state with HAVING clause
|
|
172
|
+
*/
|
|
108
173
|
withHaving(expr: ExpressionNode): SelectQueryState {
|
|
109
174
|
const combined = this.combineExpressions(this.state.ast.having, expr);
|
|
110
175
|
return this.state.withHaving(combined);
|
|
111
176
|
}
|
|
112
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Adds an ORDER BY clause to the query
|
|
180
|
+
* @param col - Column to order by
|
|
181
|
+
* @param direction - Order direction (ASC/DESC)
|
|
182
|
+
* @returns Updated query state with ORDER BY clause
|
|
183
|
+
*/
|
|
113
184
|
withOrderBy(col: ColumnDef | ColumnNode, direction: OrderDirection): SelectQueryState {
|
|
114
185
|
const node = buildColumnNode(this.table, col);
|
|
115
186
|
return this.state.withOrderBy([{ type: 'OrderBy', column: node, direction }]);
|
|
116
187
|
}
|
|
117
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Adds a DISTINCT clause to the query
|
|
191
|
+
* @param cols - Columns to make distinct
|
|
192
|
+
* @returns Updated query state with DISTINCT clause
|
|
193
|
+
*/
|
|
118
194
|
withDistinct(cols: ColumnNode[]): SelectQueryState {
|
|
119
195
|
return this.state.withDistinct(cols);
|
|
120
196
|
}
|
|
121
197
|
|
|
198
|
+
/**
|
|
199
|
+
* Adds a LIMIT clause to the query
|
|
200
|
+
* @param limit - Maximum number of rows to return
|
|
201
|
+
* @returns Updated query state with LIMIT clause
|
|
202
|
+
*/
|
|
122
203
|
withLimit(limit: number): SelectQueryState {
|
|
123
204
|
return this.state.withLimit(limit);
|
|
124
205
|
}
|
|
125
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Adds an OFFSET clause to the query
|
|
209
|
+
* @param offset - Number of rows to skip
|
|
210
|
+
* @returns Updated query state with OFFSET clause
|
|
211
|
+
*/
|
|
126
212
|
withOffset(offset: number): SelectQueryState {
|
|
127
213
|
return this.state.withOffset(offset);
|
|
128
214
|
}
|
|
129
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Combines expressions with AND operator
|
|
218
|
+
* @param existing - Existing expression
|
|
219
|
+
* @param next - New expression to combine
|
|
220
|
+
* @returns Combined expression
|
|
221
|
+
*/
|
|
130
222
|
private combineExpressions(existing: ExpressionNode | undefined, next: ExpressionNode): ExpressionNode {
|
|
131
223
|
return existing ? and(existing, next) : next;
|
|
132
224
|
}
|
|
133
225
|
|
|
134
|
-
private parseRawColumn(col: string): ColumnNode {
|
|
135
|
-
if (col.includes('(')) {
|
|
136
|
-
const [fn, rest] = col.split('(');
|
|
137
|
-
const colName = rest.replace(')', '');
|
|
138
|
-
const [table, name] = colName.includes('.') ? colName.split('.') : [this.table.name, colName];
|
|
139
|
-
return { type: 'Column', table, name, alias: col };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (col.includes('.')) {
|
|
143
|
-
const [potentialCteName, columnName] = col.split('.');
|
|
144
|
-
const hasCte = this.state.ast.ctes && this.state.ast.ctes.some(cte => cte.name === potentialCteName);
|
|
145
|
-
|
|
146
|
-
if (hasCte) {
|
|
147
|
-
return { type: 'Column', table: this.table.name, name: col };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
return { type: 'Column', table: potentialCteName, name: columnName };
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return { type: 'Column', table: this.table.name, name: col };
|
|
154
|
-
}
|
|
155
226
|
}
|
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table';
|
|
2
2
|
import { RelationDef, RelationKinds } from '../schema/relation';
|
|
3
3
|
import { ExpressionNode, eq, and } from '../ast/expression';
|
|
4
|
+
import { findPrimaryKey } from './hydration-planner';
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Utility function to handle unreachable code paths
|
|
8
|
+
* @param value - Value that should never occur
|
|
9
|
+
* @throws Error indicating unhandled relation type
|
|
10
|
+
*/
|
|
5
11
|
const assertNever = (value: never): never => {
|
|
6
12
|
throw new Error(`Unhandled relation type: ${JSON.stringify(value)}`);
|
|
7
13
|
};
|
|
8
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Builds the base condition for a relation join
|
|
17
|
+
* @param root - Root table definition
|
|
18
|
+
* @param relation - Relation definition
|
|
19
|
+
* @returns Expression node representing the join condition
|
|
20
|
+
*/
|
|
9
21
|
const baseRelationCondition = (root: TableDef, relation: RelationDef): ExpressionNode => {
|
|
10
|
-
const
|
|
22
|
+
const defaultLocalKey =
|
|
23
|
+
relation.type === RelationKinds.HasMany
|
|
24
|
+
? findPrimaryKey(root)
|
|
25
|
+
: findPrimaryKey(relation.target);
|
|
26
|
+
const localKey = relation.localKey || defaultLocalKey;
|
|
11
27
|
|
|
12
28
|
switch (relation.type) {
|
|
13
29
|
case RelationKinds.HasMany:
|
|
@@ -25,6 +41,13 @@ const baseRelationCondition = (root: TableDef, relation: RelationDef): Expressio
|
|
|
25
41
|
}
|
|
26
42
|
};
|
|
27
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Builds a relation join condition with optional extra conditions
|
|
46
|
+
* @param root - Root table definition
|
|
47
|
+
* @param relation - Relation definition
|
|
48
|
+
* @param extra - Optional additional expression to combine with AND
|
|
49
|
+
* @returns Expression node representing the complete join condition
|
|
50
|
+
*/
|
|
28
51
|
export const buildRelationJoinCondition = (
|
|
29
52
|
root: TableDef,
|
|
30
53
|
relation: RelationDef,
|
|
@@ -34,6 +57,12 @@ export const buildRelationJoinCondition = (
|
|
|
34
57
|
return extra ? and(base, extra) : base;
|
|
35
58
|
};
|
|
36
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Builds a relation correlation condition for subqueries
|
|
62
|
+
* @param root - Root table definition
|
|
63
|
+
* @param relation - Relation definition
|
|
64
|
+
* @returns Expression node representing the correlation condition
|
|
65
|
+
*/
|
|
37
66
|
export const buildRelationCorrelation = (root: TableDef, relation: RelationDef): ExpressionNode => {
|
|
38
67
|
return baseRelationCondition(root, relation);
|
|
39
68
|
};
|
|
@@ -3,25 +3,52 @@ import { ColumnDef } from '../schema/column';
|
|
|
3
3
|
import { SelectQueryState } from './select-query-state';
|
|
4
4
|
import { HydrationManager } from './hydration-manager';
|
|
5
5
|
import { ColumnNode } from '../ast/expression';
|
|
6
|
-
import { findPrimaryKey
|
|
6
|
+
import { findPrimaryKey } from './hydration-planner';
|
|
7
|
+
import { isRelationAlias } from '../utils/relation-alias';
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* Result of a relation operation
|
|
11
|
+
*/
|
|
8
12
|
export interface RelationResult {
|
|
13
|
+
/**
|
|
14
|
+
* Updated query state
|
|
15
|
+
*/
|
|
9
16
|
state: SelectQueryState;
|
|
17
|
+
/**
|
|
18
|
+
* Updated hydration manager
|
|
19
|
+
*/
|
|
10
20
|
hydration: HydrationManager;
|
|
11
21
|
}
|
|
12
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Callback function for selecting columns
|
|
25
|
+
*/
|
|
13
26
|
type SelectColumnsCallback = (
|
|
14
27
|
state: SelectQueryState,
|
|
15
28
|
hydration: HydrationManager,
|
|
16
29
|
columns: Record<string, ColumnDef>
|
|
17
30
|
) => RelationResult;
|
|
18
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Helper class for managing relation projections in queries
|
|
34
|
+
*/
|
|
19
35
|
export class RelationProjectionHelper {
|
|
36
|
+
/**
|
|
37
|
+
* Creates a new RelationProjectionHelper instance
|
|
38
|
+
* @param table - Table definition
|
|
39
|
+
* @param selectColumns - Callback for selecting columns
|
|
40
|
+
*/
|
|
20
41
|
constructor(
|
|
21
42
|
private readonly table: TableDef,
|
|
22
43
|
private readonly selectColumns: SelectColumnsCallback
|
|
23
44
|
) {}
|
|
24
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Ensures base projection is included in the query
|
|
48
|
+
* @param state - Current query state
|
|
49
|
+
* @param hydration - Hydration manager
|
|
50
|
+
* @returns Relation result with updated state and hydration
|
|
51
|
+
*/
|
|
25
52
|
ensureBaseProjection(state: SelectQueryState, hydration: HydrationManager): RelationResult {
|
|
26
53
|
const primaryKey = findPrimaryKey(this.table);
|
|
27
54
|
|
|
@@ -38,10 +65,21 @@ export class RelationProjectionHelper {
|
|
|
38
65
|
return { state, hydration };
|
|
39
66
|
}
|
|
40
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Checks if base projection exists in the query
|
|
70
|
+
* @param state - Current query state
|
|
71
|
+
* @returns True if base projection exists
|
|
72
|
+
*/
|
|
41
73
|
private hasBaseProjection(state: SelectQueryState): boolean {
|
|
42
74
|
return state.ast.columns.some(col => !isRelationAlias((col as ColumnNode).alias));
|
|
43
75
|
}
|
|
44
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Checks if primary key is selected in the query
|
|
79
|
+
* @param state - Current query state
|
|
80
|
+
* @param primaryKey - Primary key name
|
|
81
|
+
* @returns True if primary key is selected
|
|
82
|
+
*/
|
|
45
83
|
private hasPrimarySelected(state: SelectQueryState, primaryKey: string): boolean {
|
|
46
84
|
return state.ast.columns.some(col => {
|
|
47
85
|
const alias = (col as ColumnNode).alias;
|
|
@@ -50,6 +88,10 @@ export class RelationProjectionHelper {
|
|
|
50
88
|
});
|
|
51
89
|
}
|
|
52
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Gets all base columns for the table
|
|
93
|
+
* @returns Record of all table columns
|
|
94
|
+
*/
|
|
53
95
|
private getBaseColumns(): Record<string, ColumnDef> {
|
|
54
96
|
return Object.keys(this.table.columns).reduce((acc, key) => {
|
|
55
97
|
acc[key] = (this.table.columns as Record<string, ColumnDef>)[key];
|
|
@@ -7,7 +7,6 @@ import {
|
|
|
7
7
|
ExpressionNode,
|
|
8
8
|
and
|
|
9
9
|
} from '../ast/expression';
|
|
10
|
-
import { JoinNode } from '../ast/join';
|
|
11
10
|
import { SelectQueryState } from './select-query-state';
|
|
12
11
|
import { HydrationManager } from './hydration-manager';
|
|
13
12
|
import { QueryAstService } from './query-ast-service';
|
|
@@ -16,22 +15,40 @@ import { RelationProjectionHelper } from './relation-projection-helper';
|
|
|
16
15
|
import type { RelationResult } from './relation-projection-helper';
|
|
17
16
|
import { buildRelationJoinCondition, buildRelationCorrelation } from './relation-conditions';
|
|
18
17
|
import { JoinKind, JOIN_KINDS } from '../constants/sql';
|
|
18
|
+
import { RelationIncludeJoinKind } from './relation-types';
|
|
19
|
+
import { createJoinNode } from '../utils/join-node';
|
|
20
|
+
import { makeRelationAlias } from '../utils/relation-alias';
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
/**
|
|
23
|
+
* Service for handling relation operations (joins, includes, etc.)
|
|
24
|
+
*/
|
|
22
25
|
export class RelationService {
|
|
23
26
|
private readonly projectionHelper: RelationProjectionHelper;
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Creates a new RelationService instance
|
|
30
|
+
* @param table - Table definition
|
|
31
|
+
* @param state - Current query state
|
|
32
|
+
* @param hydration - Hydration manager
|
|
33
|
+
*/
|
|
25
34
|
constructor(
|
|
26
35
|
private readonly table: TableDef,
|
|
27
36
|
private readonly state: SelectQueryState,
|
|
28
|
-
private readonly hydration: HydrationManager
|
|
37
|
+
private readonly hydration: HydrationManager,
|
|
38
|
+
private readonly createQueryAstService: (table: TableDef, state: SelectQueryState) => QueryAstService
|
|
29
39
|
) {
|
|
30
40
|
this.projectionHelper = new RelationProjectionHelper(table, (state, hydration, columns) =>
|
|
31
41
|
this.selectColumns(state, hydration, columns)
|
|
32
42
|
);
|
|
33
43
|
}
|
|
34
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Joins a relation to the query
|
|
47
|
+
* @param relationName - Name of the relation to join
|
|
48
|
+
* @param joinKind - Type of join to use
|
|
49
|
+
* @param extraCondition - Additional join condition
|
|
50
|
+
* @returns Relation result with updated state and hydration
|
|
51
|
+
*/
|
|
35
52
|
joinRelation(
|
|
36
53
|
relationName: string,
|
|
37
54
|
joinKind: JoinKind,
|
|
@@ -41,6 +58,12 @@ export class RelationService {
|
|
|
41
58
|
return { state: nextState, hydration: this.hydration };
|
|
42
59
|
}
|
|
43
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Matches records based on a relation with an optional predicate
|
|
63
|
+
* @param relationName - Name of the relation to match
|
|
64
|
+
* @param predicate - Optional predicate expression
|
|
65
|
+
* @returns Relation result with updated state and hydration
|
|
66
|
+
*/
|
|
44
67
|
match(
|
|
45
68
|
relationName: string,
|
|
46
69
|
predicate?: ExpressionNode
|
|
@@ -53,6 +76,12 @@ export class RelationService {
|
|
|
53
76
|
return { state: nextState, hydration: joined.hydration };
|
|
54
77
|
}
|
|
55
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Includes a relation in the query result
|
|
81
|
+
* @param relationName - Name of the relation to include
|
|
82
|
+
* @param options - Options for relation inclusion
|
|
83
|
+
* @returns Relation result with updated state and hydration
|
|
84
|
+
*/
|
|
56
85
|
include(
|
|
57
86
|
relationName: string,
|
|
58
87
|
options?: { columns?: string[]; aliasPrefix?: string; filter?: ExpressionNode; joinKind?: RelationIncludeJoinKind }
|
|
@@ -82,7 +111,7 @@ export class RelationService {
|
|
|
82
111
|
if (!def) {
|
|
83
112
|
throw new Error(`Column '${key}' not found on relation '${relationName}'`);
|
|
84
113
|
}
|
|
85
|
-
acc[
|
|
114
|
+
acc[makeRelationAlias(aliasPrefix, key)] = def;
|
|
86
115
|
return acc;
|
|
87
116
|
}, {} as Record<string, ColumnDef>);
|
|
88
117
|
|
|
@@ -101,6 +130,12 @@ export class RelationService {
|
|
|
101
130
|
return { state, hydration };
|
|
102
131
|
}
|
|
103
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Applies relation correlation to a query AST
|
|
135
|
+
* @param relationName - Name of the relation
|
|
136
|
+
* @param ast - Query AST to modify
|
|
137
|
+
* @returns Modified query AST with relation correlation
|
|
138
|
+
*/
|
|
104
139
|
applyRelationCorrelation(
|
|
105
140
|
relationName: string,
|
|
106
141
|
ast: SelectQueryNode
|
|
@@ -117,6 +152,14 @@ export class RelationService {
|
|
|
117
152
|
};
|
|
118
153
|
}
|
|
119
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Creates a join node for a relation
|
|
157
|
+
* @param state - Current query state
|
|
158
|
+
* @param relationName - Name of the relation
|
|
159
|
+
* @param joinKind - Type of join to use
|
|
160
|
+
* @param extraCondition - Additional join condition
|
|
161
|
+
* @returns Updated query state with join
|
|
162
|
+
*/
|
|
120
163
|
private withJoin(
|
|
121
164
|
state: SelectQueryState,
|
|
122
165
|
relationName: string,
|
|
@@ -126,17 +169,18 @@ export class RelationService {
|
|
|
126
169
|
const relation = this.getRelation(relationName);
|
|
127
170
|
const condition = buildRelationJoinCondition(this.table, relation, extraCondition);
|
|
128
171
|
|
|
129
|
-
const joinNode
|
|
130
|
-
type: 'Join',
|
|
131
|
-
kind: joinKind,
|
|
132
|
-
table: { type: 'Table', name: relation.target.name },
|
|
133
|
-
condition,
|
|
134
|
-
relationName
|
|
135
|
-
};
|
|
172
|
+
const joinNode = createJoinNode(joinKind, relation.target.name, condition, relationName);
|
|
136
173
|
|
|
137
174
|
return this.astService(state).withJoin(joinNode);
|
|
138
175
|
}
|
|
139
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Selects columns for a relation
|
|
179
|
+
* @param state - Current query state
|
|
180
|
+
* @param hydration - Hydration manager
|
|
181
|
+
* @param columns - Columns to select
|
|
182
|
+
* @returns Relation result with updated state and hydration
|
|
183
|
+
*/
|
|
140
184
|
private selectColumns(
|
|
141
185
|
state: SelectQueryState,
|
|
142
186
|
hydration: HydrationManager,
|
|
@@ -149,6 +193,12 @@ export class RelationService {
|
|
|
149
193
|
};
|
|
150
194
|
}
|
|
151
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Gets a relation definition by name
|
|
198
|
+
* @param relationName - Name of the relation
|
|
199
|
+
* @returns Relation definition
|
|
200
|
+
* @throws Error if relation is not found
|
|
201
|
+
*/
|
|
152
202
|
private getRelation(relationName: string): RelationDef {
|
|
153
203
|
const relation = this.table.relations[relationName];
|
|
154
204
|
if (!relation) {
|
|
@@ -158,8 +208,13 @@ export class RelationService {
|
|
|
158
208
|
return relation;
|
|
159
209
|
}
|
|
160
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Creates a QueryAstService instance
|
|
213
|
+
* @param state - Current query state
|
|
214
|
+
* @returns QueryAstService instance
|
|
215
|
+
*/
|
|
161
216
|
private astService(state: SelectQueryState = this.state): QueryAstService {
|
|
162
|
-
return
|
|
217
|
+
return this.createQueryAstService(this.table, state);
|
|
163
218
|
}
|
|
164
219
|
}
|
|
165
220
|
|