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.
Files changed (46) hide show
  1. package/README.md +23 -13
  2. package/dist/index.cjs +1750 -733
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +244 -157
  5. package/dist/index.d.ts +244 -157
  6. package/dist/index.js +1745 -733
  7. package/dist/index.js.map +1 -1
  8. package/package.json +69 -69
  9. package/src/core/ddl/schema-generator.ts +44 -1
  10. package/src/decorators/bootstrap.ts +186 -113
  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 +18 -13
  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 -343
  21. package/src/orm/execute.ts +87 -20
  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 -309
  28. package/src/orm/relations/belongs-to.ts +2 -2
  29. package/src/orm/relations/has-many.ts +23 -9
  30. package/src/orm/relations/has-one.ts +2 -2
  31. package/src/orm/relations/many-to-many.ts +29 -14
  32. package/src/orm/save-graph-types.ts +2 -2
  33. package/src/orm/save-graph.ts +18 -18
  34. package/src/query-builder/relation-conditions.ts +80 -59
  35. package/src/query-builder/relation-cte-builder.ts +63 -0
  36. package/src/query-builder/relation-filter-utils.ts +159 -0
  37. package/src/query-builder/relation-include-strategies.ts +177 -0
  38. package/src/query-builder/relation-join-planner.ts +80 -0
  39. package/src/query-builder/relation-service.ts +103 -159
  40. package/src/query-builder/relation-types.ts +43 -12
  41. package/src/query-builder/select/projection-facet.ts +23 -23
  42. package/src/query-builder/select/select-operations.ts +145 -0
  43. package/src/query-builder/select.ts +373 -426
  44. package/src/schema/relation.ts +22 -18
  45. package/src/schema/table.ts +22 -9
  46. 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, RelationKinds, BelongsToManyRelation } from '../schema/relation.js';
4
- import { SelectQueryNode } from '../core/ast/query.js';
5
- import {
6
- ColumnNode,
7
- ExpressionNode,
8
- and
9
- } from '../core/ast/expression.js';
10
- import { SelectQueryState } from './select-query-state.js';
11
- import { HydrationManager } from './hydration-manager.js';
12
- import { QueryAstService } from './query-ast-service.js';
13
- import { findPrimaryKey } from './hydration-planner.js';
14
- import { RelationProjectionHelper } from './relation-projection-helper.js';
15
- import type { RelationResult } from './relation-projection-helper.js';
16
- import {
17
- buildRelationJoinCondition,
18
- buildRelationCorrelation,
19
- buildBelongsToManyJoins
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
- ): RelationResult {
63
- const nextState = this.withJoin(this.state, relationName, joinKind, extraCondition);
64
- return { state: nextState, hydration: this.hydration };
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
- if (!alreadyJoined) {
100
- const joined = this.joinRelation(relationName, options?.joinKind ?? JOIN_KINDS.LEFT, options?.filter);
101
- state = joined.state;
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 targetColumns = options?.columns?.length
109
- ? options.columns
110
- : Object.keys(relation.target.columns);
111
-
112
- const buildTypedSelection = (
113
- columns: Record<string, ColumnDef>,
114
- prefix: string,
115
- keys: string[],
116
- missingMsg: (col: string) => string
117
- ): Record<string, ColumnDef> => {
118
- return keys.reduce((acc, key) => {
119
- const def = columns[key];
120
- if (!def) {
121
- throw new Error(missingMsg(key));
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
- * Creates a join node for a relation
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 { ...context, state: this.columnSelector.select(context, columns).state };
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 { ...context, state: this.columnSelector.selectRaw(context, cols).state };
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 { ...context, state: this.columnSelector.selectSubquery(context, alias, query).state };
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 { ...context, state: this.columnSelector.distinct(context, cols).state };
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
+ }