metal-orm 1.0.57 → 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.
- package/README.md +23 -13
- package/dist/index.cjs +1750 -733
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +244 -157
- package/dist/index.d.ts +244 -157
- package/dist/index.js +1745 -733
- package/dist/index.js.map +1 -1
- package/package.json +69 -69
- package/src/core/ddl/schema-generator.ts +44 -1
- package/src/decorators/bootstrap.ts +186 -113
- package/src/decorators/column-decorator.ts +8 -49
- package/src/decorators/decorator-metadata.ts +10 -46
- package/src/decorators/entity.ts +30 -40
- package/src/decorators/relations.ts +30 -56
- package/src/orm/entity-hydration.ts +72 -0
- package/src/orm/entity-meta.ts +18 -13
- package/src/orm/entity-metadata.ts +240 -238
- package/src/orm/entity-relation-cache.ts +39 -0
- package/src/orm/entity-relations.ts +207 -0
- package/src/orm/entity.ts +124 -343
- package/src/orm/execute.ts +87 -20
- package/src/orm/lazy-batch/belongs-to-many.ts +134 -0
- package/src/orm/lazy-batch/belongs-to.ts +108 -0
- package/src/orm/lazy-batch/has-many.ts +69 -0
- package/src/orm/lazy-batch/has-one.ts +68 -0
- package/src/orm/lazy-batch/shared.ts +125 -0
- package/src/orm/lazy-batch.ts +4 -309
- package/src/orm/relations/belongs-to.ts +2 -2
- package/src/orm/relations/has-many.ts +23 -9
- package/src/orm/relations/has-one.ts +2 -2
- package/src/orm/relations/many-to-many.ts +29 -14
- package/src/orm/save-graph-types.ts +2 -2
- package/src/orm/save-graph.ts +18 -18
- package/src/query-builder/relation-conditions.ts +80 -59
- package/src/query-builder/relation-cte-builder.ts +63 -0
- package/src/query-builder/relation-filter-utils.ts +159 -0
- package/src/query-builder/relation-include-strategies.ts +177 -0
- package/src/query-builder/relation-join-planner.ts +80 -0
- package/src/query-builder/relation-service.ts +103 -159
- package/src/query-builder/relation-types.ts +43 -12
- package/src/query-builder/select/projection-facet.ts +23 -23
- package/src/query-builder/select/select-operations.ts +145 -0
- package/src/query-builder/select.ts +373 -426
- package/src/schema/relation.ts +22 -18
- package/src/schema/table.ts +22 -9
- package/src/schema/types.ts +103 -84
|
@@ -1,35 +1,30 @@
|
|
|
1
1
|
import { TableDef } from '../schema/table.js';
|
|
2
2
|
import { ColumnDef } from '../schema/column-types.js';
|
|
3
|
-
import { RelationDef
|
|
4
|
-
import { SelectQueryNode } from '../core/ast/query.js';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from '
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from './relation-conditions.js';
|
|
21
|
-
import { JoinKind, JOIN_KINDS } from '../core/sql/sql.js';
|
|
22
|
-
import { RelationIncludeOptions } from './relation-types.js';
|
|
23
|
-
import { createJoinNode } from '../core/ast/join-node.js';
|
|
24
|
-
import { getJoinRelationName } from '../core/ast/join-metadata.js';
|
|
25
|
-
import { makeRelationAlias } from './relation-alias.js';
|
|
26
|
-
import { buildDefaultPivotColumns } from './relation-utils.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';
|
|
14
|
+
import { RelationIncludeOptions } from './relation-types.js';
|
|
15
|
+
import { getJoinRelationName } from '../core/ast/join-metadata.js';
|
|
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';
|
|
27
20
|
|
|
28
21
|
/**
|
|
29
22
|
* Service for handling relation operations (joins, includes, etc.)
|
|
30
23
|
*/
|
|
31
|
-
export class RelationService {
|
|
32
|
-
private readonly projectionHelper: RelationProjectionHelper;
|
|
24
|
+
export class RelationService {
|
|
25
|
+
private readonly projectionHelper: RelationProjectionHelper;
|
|
26
|
+
private readonly joinPlanner: RelationJoinPlanner;
|
|
27
|
+
private readonly cteBuilder: RelationCteBuilder;
|
|
33
28
|
|
|
34
29
|
/**
|
|
35
30
|
* Creates a new RelationService instance
|
|
@@ -42,11 +37,13 @@ export class RelationService {
|
|
|
42
37
|
private readonly state: SelectQueryState,
|
|
43
38
|
private readonly hydration: HydrationManager,
|
|
44
39
|
private readonly createQueryAstService: (table: TableDef, state: SelectQueryState) => QueryAstService
|
|
45
|
-
) {
|
|
46
|
-
this.projectionHelper = new RelationProjectionHelper(table, (state, hydration, columns) =>
|
|
47
|
-
this.selectColumns(state, hydration, columns)
|
|
48
|
-
);
|
|
49
|
-
|
|
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
|
+
}
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
49
|
* Joins a relation to the query
|
|
@@ -58,11 +55,20 @@ export class RelationService {
|
|
|
58
55
|
joinRelation(
|
|
59
56
|
relationName: string,
|
|
60
57
|
joinKind: JoinKind,
|
|
61
|
-
extraCondition?: ExpressionNode
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
extraCondition?: ExpressionNode,
|
|
59
|
+
tableSource?: TableSourceNode
|
|
60
|
+
): RelationResult {
|
|
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
|
+
);
|
|
70
|
+
return { state: nextState, hydration: this.hydration };
|
|
71
|
+
}
|
|
66
72
|
|
|
67
73
|
/**
|
|
68
74
|
* Matches records based on a relation with an optional predicate
|
|
@@ -95,94 +101,60 @@ export class RelationService {
|
|
|
95
101
|
const relation = this.getRelation(relationName);
|
|
96
102
|
const aliasPrefix = options?.aliasPrefix ?? relationName;
|
|
97
103
|
const alreadyJoined = state.ast.joins.some(j => getJoinRelationName(j) === relationName);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
104
|
+
const { selfFilters, crossFilters } = splitFilterExpressions(
|
|
105
|
+
options?.filter,
|
|
106
|
+
new Set([relation.target.name])
|
|
107
|
+
);
|
|
108
|
+
const canUseCte = !alreadyJoined && selfFilters.length > 0;
|
|
109
|
+
const joinFilters = [...crossFilters];
|
|
110
|
+
if (!canUseCte) {
|
|
111
|
+
joinFilters.push(...selfFilters);
|
|
102
112
|
}
|
|
113
|
+
const joinCondition = this.combineWithAnd(joinFilters);
|
|
114
|
+
|
|
115
|
+
let tableSourceOverride: TableNode | undefined;
|
|
116
|
+
if (canUseCte) {
|
|
117
|
+
const predicate = this.combineWithAnd(selfFilters);
|
|
118
|
+
const cteInfo = this.cteBuilder.createFilteredRelationCte(
|
|
119
|
+
state,
|
|
120
|
+
relationName,
|
|
121
|
+
relation,
|
|
122
|
+
predicate
|
|
123
|
+
);
|
|
124
|
+
state = cteInfo.state;
|
|
125
|
+
tableSourceOverride = cteInfo.table;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!alreadyJoined) {
|
|
129
|
+
state = this.joinPlanner.withJoin(
|
|
130
|
+
state,
|
|
131
|
+
relationName,
|
|
132
|
+
relation,
|
|
133
|
+
options?.joinKind ?? JOIN_KINDS.LEFT,
|
|
134
|
+
joinCondition,
|
|
135
|
+
tableSourceOverride
|
|
136
|
+
);
|
|
137
|
+
}
|
|
103
138
|
|
|
104
139
|
const projectionResult = this.projectionHelper.ensureBaseProjection(state, hydration);
|
|
105
140
|
state = projectionResult.state;
|
|
106
141
|
hydration = projectionResult.hydration;
|
|
107
142
|
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
acc[makeRelationAlias(prefix, key)] = def;
|
|
124
|
-
return acc;
|
|
125
|
-
}, {} as Record<string, ColumnDef>);
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const targetSelection = buildTypedSelection(
|
|
129
|
-
relation.target.columns as Record<string, ColumnDef>,
|
|
130
|
-
aliasPrefix,
|
|
131
|
-
targetColumns,
|
|
132
|
-
key => `Column '${key}' not found on relation '${relationName}'`
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
if (relation.type !== RelationKinds.BelongsToMany) {
|
|
136
|
-
const relationSelectionResult = this.selectColumns(state, hydration, targetSelection);
|
|
137
|
-
state = relationSelectionResult.state;
|
|
138
|
-
hydration = relationSelectionResult.hydration;
|
|
139
|
-
|
|
140
|
-
hydration = hydration.onRelationIncluded(
|
|
141
|
-
state,
|
|
142
|
-
relation,
|
|
143
|
-
relationName,
|
|
144
|
-
aliasPrefix,
|
|
145
|
-
targetColumns
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
return { state, hydration };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const many = relation as BelongsToManyRelation;
|
|
152
|
-
const pivotAliasPrefix = options?.pivot?.aliasPrefix ?? `${aliasPrefix}_pivot`;
|
|
153
|
-
const pivotPk = many.pivotPrimaryKey || findPrimaryKey(many.pivotTable);
|
|
154
|
-
const pivotColumns =
|
|
155
|
-
options?.pivot?.columns ??
|
|
156
|
-
many.defaultPivotColumns ??
|
|
157
|
-
buildDefaultPivotColumns(many, pivotPk);
|
|
158
|
-
|
|
159
|
-
const pivotSelection = buildTypedSelection(
|
|
160
|
-
many.pivotTable.columns as Record<string, ColumnDef>,
|
|
161
|
-
pivotAliasPrefix,
|
|
162
|
-
pivotColumns,
|
|
163
|
-
key => `Column '${key}' not found on pivot table '${many.pivotTable.name}'`
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const combinedSelection = {
|
|
167
|
-
...targetSelection,
|
|
168
|
-
...pivotSelection
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const relationSelectionResult = this.selectColumns(state, hydration, combinedSelection);
|
|
172
|
-
state = relationSelectionResult.state;
|
|
173
|
-
hydration = relationSelectionResult.hydration;
|
|
174
|
-
|
|
175
|
-
hydration = hydration.onRelationIncluded(
|
|
176
|
-
state,
|
|
177
|
-
relation,
|
|
178
|
-
relationName,
|
|
179
|
-
aliasPrefix,
|
|
180
|
-
targetColumns,
|
|
181
|
-
{ aliasPrefix: pivotAliasPrefix, columns: pivotColumns }
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
return { state, hydration };
|
|
185
|
-
}
|
|
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
|
+
}
|
|
186
158
|
|
|
187
159
|
/**
|
|
188
160
|
* Applies relation correlation to a query AST
|
|
@@ -211,47 +183,8 @@ export class RelationService {
|
|
|
211
183
|
};
|
|
212
184
|
}
|
|
213
185
|
|
|
214
|
-
/**
|
|
215
|
-
*
|
|
216
|
-
* @param state - Current query state
|
|
217
|
-
* @param relationName - Name of the relation
|
|
218
|
-
* @param joinKind - Type of join to use
|
|
219
|
-
* @param extraCondition - Additional join condition
|
|
220
|
-
* @returns Updated query state with join
|
|
221
|
-
*/
|
|
222
|
-
private withJoin(
|
|
223
|
-
state: SelectQueryState,
|
|
224
|
-
relationName: string,
|
|
225
|
-
joinKind: JoinKind,
|
|
226
|
-
extraCondition?: ExpressionNode
|
|
227
|
-
): SelectQueryState {
|
|
228
|
-
const relation = this.getRelation(relationName);
|
|
229
|
-
const rootAlias = state.ast.from.type === 'Table' ? state.ast.from.alias : undefined;
|
|
230
|
-
if (relation.type === RelationKinds.BelongsToMany) {
|
|
231
|
-
const joins = buildBelongsToManyJoins(
|
|
232
|
-
this.table,
|
|
233
|
-
relationName,
|
|
234
|
-
relation as BelongsToManyRelation,
|
|
235
|
-
joinKind,
|
|
236
|
-
extraCondition,
|
|
237
|
-
rootAlias
|
|
238
|
-
);
|
|
239
|
-
return joins.reduce((current, join) => this.astService(current).withJoin(join), state);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const condition = buildRelationJoinCondition(this.table, relation, extraCondition, rootAlias);
|
|
243
|
-
const joinNode = createJoinNode(
|
|
244
|
-
joinKind,
|
|
245
|
-
{ type: 'Table', name: relation.target.name, schema: relation.target.schema },
|
|
246
|
-
condition,
|
|
247
|
-
relationName
|
|
248
|
-
);
|
|
249
|
-
|
|
250
|
-
return this.astService(state).withJoin(joinNode);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* Selects columns for a relation
|
|
186
|
+
/**
|
|
187
|
+
* Selects columns for a relation
|
|
255
188
|
* @param state - Current query state
|
|
256
189
|
* @param hydration - Hydration manager
|
|
257
190
|
* @param columns - Columns to select
|
|
@@ -269,6 +202,17 @@ export class RelationService {
|
|
|
269
202
|
};
|
|
270
203
|
}
|
|
271
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
|
+
|
|
272
216
|
/**
|
|
273
217
|
* Gets a relation definition by name
|
|
274
218
|
* @param relationName - Name of the relation
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
import { ExpressionNode } from '../core/ast/expression.js';
|
|
2
|
-
import { JOIN_KINDS } from '../core/sql/sql.js';
|
|
1
|
+
import { ExpressionNode } from '../core/ast/expression.js';
|
|
2
|
+
import { JOIN_KINDS } from '../core/sql/sql.js';
|
|
3
|
+
import { TableDef } from '../schema/table.js';
|
|
4
|
+
import { BelongsToManyRelation, RelationDef } from '../schema/relation.js';
|
|
5
|
+
import { RelationTargetTable } from '../schema/types.js';
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Join kinds allowed when including a relation using `.include(...)`.
|
|
@@ -9,13 +12,41 @@ export type RelationIncludeJoinKind = typeof JOIN_KINDS.LEFT | typeof JOIN_KINDS
|
|
|
9
12
|
/**
|
|
10
13
|
* Options for including a relation in a query
|
|
11
14
|
*/
|
|
12
|
-
export interface RelationIncludeOptions {
|
|
13
|
-
columns?: string[];
|
|
14
|
-
aliasPrefix?: string;
|
|
15
|
-
filter?: ExpressionNode;
|
|
16
|
-
joinKind?: RelationIncludeJoinKind;
|
|
17
|
-
pivot?: {
|
|
18
|
-
columns?: string[];
|
|
19
|
-
aliasPrefix?: string;
|
|
20
|
-
};
|
|
21
|
-
}
|
|
15
|
+
export interface RelationIncludeOptions {
|
|
16
|
+
columns?: readonly string[];
|
|
17
|
+
aliasPrefix?: string;
|
|
18
|
+
filter?: ExpressionNode;
|
|
19
|
+
joinKind?: RelationIncludeJoinKind;
|
|
20
|
+
pivot?: {
|
|
21
|
+
columns?: readonly string[];
|
|
22
|
+
aliasPrefix?: string;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type ColumnKeys<T> =
|
|
27
|
+
T extends { columns: infer Columns }
|
|
28
|
+
? keyof Columns & string
|
|
29
|
+
: string;
|
|
30
|
+
|
|
31
|
+
type PivotColumnKeys<TPivot> = ColumnKeys<TPivot> extends never ? string : ColumnKeys<TPivot>;
|
|
32
|
+
|
|
33
|
+
export type RelationTargetColumns<TRel extends RelationDef> =
|
|
34
|
+
ColumnKeys<RelationTargetTable<TRel>>;
|
|
35
|
+
|
|
36
|
+
export type BelongsToManyPivotColumns<TRel extends RelationDef> =
|
|
37
|
+
TRel extends BelongsToManyRelation<TableDef, infer TPivot>
|
|
38
|
+
? PivotColumnKeys<TPivot>
|
|
39
|
+
: never;
|
|
40
|
+
|
|
41
|
+
export type TypedRelationIncludeOptions<TRel extends RelationDef> =
|
|
42
|
+
TRel extends BelongsToManyRelation
|
|
43
|
+
? Omit<RelationIncludeOptions, 'columns' | 'pivot'> & {
|
|
44
|
+
columns?: readonly RelationTargetColumns<TRel>[];
|
|
45
|
+
pivot?: {
|
|
46
|
+
columns?: readonly BelongsToManyPivotColumns<TRel>[];
|
|
47
|
+
aliasPrefix?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
: Omit<RelationIncludeOptions, 'columns' | 'pivot'> & {
|
|
51
|
+
columns?: readonly RelationTargetColumns<TRel>[];
|
|
52
|
+
};
|
|
@@ -22,12 +22,12 @@ export class SelectProjectionFacet {
|
|
|
22
22
|
* @param columns - Columns to select
|
|
23
23
|
* @returns Updated query context with selected columns
|
|
24
24
|
*/
|
|
25
|
-
select(
|
|
26
|
-
context: SelectQueryBuilderContext,
|
|
27
|
-
columns: Record<string, ColumnSelectionValue>
|
|
28
|
-
): SelectQueryBuilderContext {
|
|
29
|
-
return
|
|
30
|
-
}
|
|
25
|
+
select(
|
|
26
|
+
context: SelectQueryBuilderContext,
|
|
27
|
+
columns: Record<string, ColumnSelectionValue>
|
|
28
|
+
): SelectQueryBuilderContext {
|
|
29
|
+
return this.columnSelector.select(context, columns);
|
|
30
|
+
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Selects raw column expressions
|
|
@@ -35,9 +35,9 @@ export class SelectProjectionFacet {
|
|
|
35
35
|
* @param cols - Raw column expressions
|
|
36
36
|
* @returns Updated query context with raw column selections
|
|
37
37
|
*/
|
|
38
|
-
selectRaw(context: SelectQueryBuilderContext, cols: string[]): SelectQueryBuilderContext {
|
|
39
|
-
return
|
|
40
|
-
}
|
|
38
|
+
selectRaw(context: SelectQueryBuilderContext, cols: string[]): SelectQueryBuilderContext {
|
|
39
|
+
return this.columnSelector.selectRaw(context, cols);
|
|
40
|
+
}
|
|
41
41
|
|
|
42
42
|
/**
|
|
43
43
|
* Selects a subquery as a column
|
|
@@ -46,13 +46,13 @@ export class SelectProjectionFacet {
|
|
|
46
46
|
* @param query - Subquery to select
|
|
47
47
|
* @returns Updated query context with subquery selection
|
|
48
48
|
*/
|
|
49
|
-
selectSubquery(
|
|
50
|
-
context: SelectQueryBuilderContext,
|
|
51
|
-
alias: string,
|
|
52
|
-
query: SelectQueryNode
|
|
53
|
-
): SelectQueryBuilderContext {
|
|
54
|
-
return
|
|
55
|
-
}
|
|
49
|
+
selectSubquery(
|
|
50
|
+
context: SelectQueryBuilderContext,
|
|
51
|
+
alias: string,
|
|
52
|
+
query: SelectQueryNode
|
|
53
|
+
): SelectQueryBuilderContext {
|
|
54
|
+
return this.columnSelector.selectSubquery(context, alias, query);
|
|
55
|
+
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Adds DISTINCT clause to the query
|
|
@@ -60,11 +60,11 @@ export class SelectProjectionFacet {
|
|
|
60
60
|
* @param cols - Columns to make distinct
|
|
61
61
|
* @returns Updated query context with DISTINCT clause
|
|
62
62
|
*/
|
|
63
|
-
distinct(
|
|
64
|
-
context: SelectQueryBuilderContext,
|
|
65
|
-
cols: (ColumnDef | ColumnNode)[]
|
|
66
|
-
): SelectQueryBuilderContext {
|
|
67
|
-
return
|
|
68
|
-
}
|
|
69
|
-
}
|
|
63
|
+
distinct(
|
|
64
|
+
context: SelectQueryBuilderContext,
|
|
65
|
+
cols: (ColumnDef | ColumnNode)[]
|
|
66
|
+
): SelectQueryBuilderContext {
|
|
67
|
+
return this.columnSelector.distinct(context, cols);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
70
|
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { TableDef } from '../../schema/table.js';
|
|
2
|
+
import { ColumnDef } from '../../schema/column-types.js';
|
|
3
|
+
import { OrderingTerm, SelectQueryNode } from '../../core/ast/query.js';
|
|
4
|
+
import { FunctionNode, ExpressionNode, exists, notExists } from '../../core/ast/expression.js';
|
|
5
|
+
import { derivedTable } from '../../core/ast/builders.js';
|
|
6
|
+
import { SelectQueryState } from '../select-query-state.js';
|
|
7
|
+
import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps.js';
|
|
8
|
+
import { SelectPredicateFacet } from './predicate-facet.js';
|
|
9
|
+
import { SelectRelationFacet } from './relation-facet.js';
|
|
10
|
+
import { ORDER_DIRECTIONS, OrderDirection } from '../../core/sql/sql.js';
|
|
11
|
+
import { OrmSession } from '../../orm/orm-session.js';
|
|
12
|
+
import { EntityInstance } from '../../schema/types.js';
|
|
13
|
+
import type { SelectQueryBuilder } from '../select.js';
|
|
14
|
+
|
|
15
|
+
export type WhereHasOptions = {
|
|
16
|
+
correlate?: ExpressionNode;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type RelationCallback = <TChildTable extends TableDef>(
|
|
20
|
+
qb: SelectQueryBuilder<unknown, TChildTable>
|
|
21
|
+
) => SelectQueryBuilder<unknown, TChildTable>;
|
|
22
|
+
|
|
23
|
+
type ChildBuilderFactory = <R, TChild extends TableDef>(table: TChild) => SelectQueryBuilder<R, TChild>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds a new query context with an ORDER BY clause applied.
|
|
27
|
+
*/
|
|
28
|
+
export function applyOrderBy(
|
|
29
|
+
context: SelectQueryBuilderContext,
|
|
30
|
+
predicateFacet: SelectPredicateFacet,
|
|
31
|
+
term: ColumnDef | OrderingTerm,
|
|
32
|
+
directionOrOptions: OrderDirection | { direction?: OrderDirection; nulls?: 'FIRST' | 'LAST'; collation?: string }
|
|
33
|
+
): SelectQueryBuilderContext {
|
|
34
|
+
const options =
|
|
35
|
+
typeof directionOrOptions === 'string' ? { direction: directionOrOptions } : directionOrOptions;
|
|
36
|
+
const dir = options.direction ?? ORDER_DIRECTIONS.ASC;
|
|
37
|
+
return predicateFacet.orderBy(context, term, dir, options.nulls, options.collation);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Runs the count query for the provided context and session.
|
|
42
|
+
*/
|
|
43
|
+
export async function executeCount(
|
|
44
|
+
context: SelectQueryBuilderContext,
|
|
45
|
+
env: SelectQueryBuilderEnvironment,
|
|
46
|
+
session: OrmSession
|
|
47
|
+
): Promise<number> {
|
|
48
|
+
const unpagedAst: SelectQueryNode = {
|
|
49
|
+
...context.state.ast,
|
|
50
|
+
orderBy: undefined,
|
|
51
|
+
limit: undefined,
|
|
52
|
+
offset: undefined
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const nextState = new SelectQueryState(env.table as TableDef, unpagedAst);
|
|
56
|
+
const nextContext: SelectQueryBuilderContext = {
|
|
57
|
+
...context,
|
|
58
|
+
state: nextState
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const subAst = nextContext.hydration.applyToAst(nextState.ast);
|
|
62
|
+
const countQuery: SelectQueryNode = {
|
|
63
|
+
type: 'SelectQuery',
|
|
64
|
+
from: derivedTable(subAst, '__metal_count'),
|
|
65
|
+
columns: [{ type: 'Function', name: 'COUNT', args: [], alias: 'total' } as FunctionNode],
|
|
66
|
+
joins: []
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const execCtx = session.getExecutionContext();
|
|
70
|
+
const compiled = execCtx.dialect.compileSelect(countQuery);
|
|
71
|
+
const results = await execCtx.interceptors.run({ sql: compiled.sql, params: compiled.params }, execCtx.executor);
|
|
72
|
+
const value = results[0]?.values?.[0]?.[0];
|
|
73
|
+
|
|
74
|
+
if (typeof value === 'number') return value;
|
|
75
|
+
if (typeof value === 'bigint') return Number(value);
|
|
76
|
+
if (typeof value === 'string') return Number(value);
|
|
77
|
+
return value === null || value === undefined ? 0 : Number(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Executes paged queries using the provided builder helpers.
|
|
82
|
+
*/
|
|
83
|
+
export async function executePagedQuery<T, TTable extends TableDef>(
|
|
84
|
+
builder: SelectQueryBuilder<T, TTable>,
|
|
85
|
+
session: OrmSession,
|
|
86
|
+
options: { page: number; pageSize: number },
|
|
87
|
+
countCallback: (session: OrmSession) => Promise<number>
|
|
88
|
+
): Promise<{ items: EntityInstance<TTable>[]; totalItems: number }> {
|
|
89
|
+
const { page, pageSize } = options;
|
|
90
|
+
|
|
91
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
92
|
+
throw new Error('executePaged: page must be an integer >= 1');
|
|
93
|
+
}
|
|
94
|
+
if (!Number.isInteger(pageSize) || pageSize < 1) {
|
|
95
|
+
throw new Error('executePaged: pageSize must be an integer >= 1');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const offset = (page - 1) * pageSize;
|
|
99
|
+
|
|
100
|
+
const [items, totalItems] = await Promise.all([
|
|
101
|
+
builder.limit(pageSize).offset(offset).execute(session),
|
|
102
|
+
countCallback(session)
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
return { items, totalItems };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Builds an EXISTS or NOT EXISTS predicate for a related table.
|
|
110
|
+
*/
|
|
111
|
+
export function buildWhereHasPredicate<TTable extends TableDef>(
|
|
112
|
+
env: SelectQueryBuilderEnvironment,
|
|
113
|
+
context: SelectQueryBuilderContext,
|
|
114
|
+
relationFacet: SelectRelationFacet,
|
|
115
|
+
createChildBuilder: ChildBuilderFactory,
|
|
116
|
+
relationName: keyof TTable['relations'] & string,
|
|
117
|
+
callbackOrOptions?: RelationCallback | WhereHasOptions,
|
|
118
|
+
maybeOptions?: WhereHasOptions,
|
|
119
|
+
negate = false
|
|
120
|
+
): ExpressionNode {
|
|
121
|
+
const relation = env.table.relations[relationName as string];
|
|
122
|
+
if (!relation) {
|
|
123
|
+
throw new Error(`Relation '${relationName}' not found on table '${env.table.name}'`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const callback = typeof callbackOrOptions === 'function' ? callbackOrOptions : undefined;
|
|
127
|
+
const options = (typeof callbackOrOptions === 'function' ? maybeOptions : callbackOrOptions) as
|
|
128
|
+
| WhereHasOptions
|
|
129
|
+
| undefined;
|
|
130
|
+
|
|
131
|
+
let subQb = createChildBuilder<unknown, TableDef>(relation.target);
|
|
132
|
+
if (callback) {
|
|
133
|
+
subQb = callback(subQb);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const subAst = subQb.getAST();
|
|
137
|
+
const finalSubAst = relationFacet.applyRelationCorrelation(
|
|
138
|
+
context,
|
|
139
|
+
relationName,
|
|
140
|
+
subAst,
|
|
141
|
+
options?.correlate
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return negate ? notExists(finalSubAst) : exists(finalSubAst);
|
|
145
|
+
}
|