metal-orm 1.0.43 → 1.0.44

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 (84) hide show
  1. package/README.md +173 -30
  2. package/dist/index.cjs +896 -476
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +1146 -275
  5. package/dist/index.d.ts +1146 -275
  6. package/dist/index.js +896 -474
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/core/ast/adapters.ts +8 -2
  10. package/src/core/ast/builders.ts +105 -81
  11. package/src/core/ast/expression-builders.ts +430 -390
  12. package/src/core/ast/expression-visitor.ts +47 -8
  13. package/src/core/ast/helpers.ts +23 -0
  14. package/src/core/ast/join-node.ts +17 -1
  15. package/src/core/ddl/dialects/base-schema-dialect.ts +7 -1
  16. package/src/core/ddl/dialects/index.ts +1 -0
  17. package/src/core/ddl/dialects/mssql-schema-dialect.ts +1 -0
  18. package/src/core/ddl/dialects/mysql-schema-dialect.ts +1 -0
  19. package/src/core/ddl/dialects/postgres-schema-dialect.ts +1 -0
  20. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +1 -0
  21. package/src/core/ddl/introspect/catalogs/index.ts +1 -0
  22. package/src/core/ddl/introspect/catalogs/postgres.ts +2 -0
  23. package/src/core/ddl/introspect/context.ts +6 -0
  24. package/src/core/ddl/introspect/functions/postgres.ts +13 -0
  25. package/src/core/ddl/introspect/mssql.ts +11 -0
  26. package/src/core/ddl/introspect/mysql.ts +2 -0
  27. package/src/core/ddl/introspect/postgres.ts +14 -0
  28. package/src/core/ddl/introspect/registry.ts +14 -0
  29. package/src/core/ddl/introspect/run-select.ts +13 -0
  30. package/src/core/ddl/introspect/sqlite.ts +22 -0
  31. package/src/core/ddl/introspect/utils.ts +18 -0
  32. package/src/core/ddl/naming-strategy.ts +6 -0
  33. package/src/core/ddl/schema-dialect.ts +19 -6
  34. package/src/core/ddl/schema-diff.ts +22 -0
  35. package/src/core/ddl/schema-generator.ts +22 -0
  36. package/src/core/ddl/schema-plan-executor.ts +6 -0
  37. package/src/core/ddl/schema-types.ts +6 -0
  38. package/src/core/dialect/abstract.ts +2 -2
  39. package/src/core/execution/pooling/pool.ts +12 -7
  40. package/src/core/functions/datetime.ts +57 -33
  41. package/src/core/functions/numeric.ts +95 -30
  42. package/src/core/functions/standard-strategy.ts +35 -0
  43. package/src/core/functions/text.ts +83 -22
  44. package/src/core/functions/types.ts +23 -8
  45. package/src/decorators/bootstrap.ts +16 -4
  46. package/src/decorators/column.ts +17 -0
  47. package/src/decorators/decorator-metadata.ts +27 -0
  48. package/src/decorators/entity.ts +8 -0
  49. package/src/decorators/index.ts +3 -0
  50. package/src/decorators/relations.ts +32 -0
  51. package/src/orm/als.ts +34 -9
  52. package/src/orm/entity-context.ts +54 -0
  53. package/src/orm/entity-metadata.ts +122 -9
  54. package/src/orm/execute.ts +15 -0
  55. package/src/orm/lazy-batch.ts +68 -98
  56. package/src/orm/relations/has-many.ts +44 -0
  57. package/src/query/index.ts +74 -0
  58. package/src/query/target.ts +46 -0
  59. package/src/query-builder/delete-query-state.ts +30 -0
  60. package/src/query-builder/delete.ts +64 -19
  61. package/src/query-builder/hydration-manager.ts +46 -0
  62. package/src/query-builder/insert-query-state.ts +30 -0
  63. package/src/query-builder/insert.ts +46 -2
  64. package/src/query-builder/query-ast-service.ts +5 -0
  65. package/src/query-builder/query-resolution.ts +78 -0
  66. package/src/query-builder/raw-column-parser.ts +5 -0
  67. package/src/query-builder/relation-alias.ts +7 -0
  68. package/src/query-builder/relation-conditions.ts +61 -48
  69. package/src/query-builder/relation-service.ts +68 -63
  70. package/src/query-builder/relation-utils.ts +3 -0
  71. package/src/query-builder/select/cte-facet.ts +40 -0
  72. package/src/query-builder/select/from-facet.ts +80 -0
  73. package/src/query-builder/select/join-facet.ts +62 -0
  74. package/src/query-builder/select/predicate-facet.ts +103 -0
  75. package/src/query-builder/select/projection-facet.ts +69 -0
  76. package/src/query-builder/select/relation-facet.ts +81 -0
  77. package/src/query-builder/select/setop-facet.ts +36 -0
  78. package/src/query-builder/select-helpers.ts +13 -0
  79. package/src/query-builder/select-query-builder-deps.ts +19 -1
  80. package/src/query-builder/select-query-state.ts +2 -1
  81. package/src/query-builder/select.ts +795 -1163
  82. package/src/query-builder/update-query-state.ts +52 -0
  83. package/src/query-builder/update.ts +69 -19
  84. package/src/schema/table-guards.ts +31 -0
@@ -251,6 +251,11 @@ export class QueryAstService {
251
251
  return existing ? and(existing, next) : next;
252
252
  }
253
253
 
254
+ /**
255
+ * Normalizes an ordering term to a standard OrderingTerm
256
+ * @param term - Column definition or ordering term to normalize
257
+ * @returns Normalized ordering term
258
+ */
254
259
  private normalizeOrderingTerm(term: ColumnDef | OrderingTerm): OrderingTerm {
255
260
  const from = this.state.ast.from;
256
261
  const tableRef = from.type === 'Table' && from.alias ? { ...this.table, alias: from.alias } : this.table;
@@ -0,0 +1,78 @@
1
+ import { SelectQueryNode, UpdateQueryNode, DeleteQueryNode, TableSourceNode } from '../core/ast/query.js';
2
+ import { TableDef } from '../schema/table.js';
3
+ import type { SelectQueryBuilder } from './select.js';
4
+ import type { UpdateQueryBuilder } from './update.js';
5
+ import type { DeleteQueryBuilder } from './delete.js';
6
+
7
+ /**
8
+ * Resolves a SelectQueryBuilder or SelectQueryNode to a SelectQueryNode AST
9
+ * @param query - Query builder or AST node
10
+ * @returns SelectQueryNode AST
11
+ */
12
+ export function resolveSelectQuery<TSub extends TableDef>(
13
+ query: SelectQueryBuilder<unknown, TSub> | SelectQueryNode
14
+ ): SelectQueryNode {
15
+ const candidate = query as { getAST?: () => SelectQueryNode };
16
+ return typeof candidate.getAST === 'function' && candidate.getAST
17
+ ? candidate.getAST()
18
+ : (query as SelectQueryNode);
19
+ }
20
+
21
+ /**
22
+ * Resolves a UpdateQueryBuilder or UpdateQueryNode to a UpdateQueryNode AST
23
+ * @param query - Query builder or AST node
24
+ * @returns UpdateQueryNode AST
25
+ */
26
+ export function resolveUpdateQuery<T>(
27
+ query: UpdateQueryBuilder<T> | UpdateQueryNode
28
+ ): UpdateQueryNode {
29
+ const candidate = query as { getAST?: () => UpdateQueryNode };
30
+ return typeof candidate.getAST === 'function' && candidate.getAST
31
+ ? candidate.getAST()
32
+ : (query as UpdateQueryNode);
33
+ }
34
+
35
+ /**
36
+ * Resolves a DeleteQueryBuilder or DeleteQueryNode to a DeleteQueryNode AST
37
+ * @param query - Query builder or AST node
38
+ * @returns DeleteQueryNode AST
39
+ */
40
+ export function resolveDeleteQuery<T>(
41
+ query: DeleteQueryBuilder<T> | DeleteQueryNode
42
+ ): DeleteQueryNode {
43
+ const candidate = query as { getAST?: () => DeleteQueryNode };
44
+ return typeof candidate.getAST === 'function' && candidate.getAST
45
+ ? candidate.getAST()
46
+ : (query as DeleteQueryNode);
47
+ }
48
+
49
+ /**
50
+ * Resolves a TableDef or TableSourceNode to a TableSourceNode
51
+ * @param source - Table definition or source node
52
+ * @returns TableSourceNode
53
+ */
54
+ export function resolveTableSource(source: TableDef | TableSourceNode): TableSourceNode {
55
+ if (isTableSourceNode(source)) {
56
+ return source;
57
+ }
58
+ return { type: 'Table', name: source.name, schema: source.schema };
59
+ }
60
+
61
+ /**
62
+ * Resolves a join target (TableDef, TableSourceNode, or string relation name)
63
+ * @param table - Join target
64
+ * @returns TableSourceNode or string
65
+ */
66
+ export function resolveJoinTarget(table: TableDef | TableSourceNode | string): TableSourceNode | string {
67
+ if (typeof table === 'string') return table;
68
+ return resolveTableSource(table);
69
+ }
70
+
71
+ /**
72
+ * Type guard to check if a value is a TableSourceNode
73
+ * @param source - Value to check
74
+ * @returns True if value is a TableSourceNode
75
+ */
76
+ function isTableSourceNode(source: TableDef | TableSourceNode): source is TableSourceNode {
77
+ return typeof (source as TableSourceNode).type === 'string';
78
+ }
@@ -4,6 +4,11 @@ import { CommonTableExpressionNode } from '../core/ast/query.js';
4
4
  /**
5
5
  * Best-effort helper that tries to convert a raw column expression into a `ColumnNode`.
6
6
  * This parser is intentionally limited; use it only for simple references or function calls.
7
+ *
8
+ * @param col - Raw column expression string (e.g., "column", "table.column", "COUNT(column)")
9
+ * @param tableName - Default table name to use when no table is specified
10
+ * @param ctes - Optional array of CTEs for context when parsing column references
11
+ * @returns A ColumnNode representing the parsed column expression
7
12
  */
8
13
  export const parseRawColumn = (
9
14
  col: string,
@@ -19,6 +19,9 @@ export interface RelationAliasParts {
19
19
 
20
20
  /**
21
21
  * Builds a relation alias from the relation name and column name components.
22
+ * @param relationName - The name of the relation
23
+ * @param columnName - The name of the column within the relation
24
+ * @returns A relation alias string in the format "relationName__columnName"
22
25
  */
23
26
  export const makeRelationAlias = (relationName: string, columnName: string): string =>
24
27
  `${relationName}${RELATION_SEPARATOR}${columnName}`;
@@ -26,6 +29,8 @@ export const makeRelationAlias = (relationName: string, columnName: string): str
26
29
  /**
27
30
  * Parses a relation alias into its relation/column components.
28
31
  * Returns `null` when the alias does not follow the `relation__column` pattern.
32
+ * @param alias - The relation alias string to parse
33
+ * @returns Parsed relation alias parts or null if not a valid relation alias
29
34
  */
30
35
  export const parseRelationAlias = (alias: string): RelationAliasParts | null => {
31
36
  const idx = alias.indexOf(RELATION_SEPARATOR);
@@ -38,6 +43,8 @@ export const parseRelationAlias = (alias: string): RelationAliasParts | null =>
38
43
 
39
44
  /**
40
45
  * Determines whether an alias represents a relation column by checking the `__` convention.
46
+ * @param alias - The alias string to check
47
+ * @returns True if the alias follows the relation alias pattern
41
48
  */
42
49
  export const isRelationAlias = (alias?: string): boolean =>
43
50
  !!alias && alias.includes(RELATION_SEPARATOR);
@@ -21,26 +21,26 @@ const assertNever = (value: never): never => {
21
21
  * @param relation - Relation definition
22
22
  * @returns Expression node representing the join condition
23
23
  */
24
- const baseRelationCondition = (root: TableDef, relation: RelationDef, rootAlias?: string): ExpressionNode => {
25
- const rootTable = rootAlias || root.name;
26
- const defaultLocalKey =
27
- relation.type === RelationKinds.HasMany || relation.type === RelationKinds.HasOne
28
- ? findPrimaryKey(root)
29
- : findPrimaryKey(relation.target);
30
- const localKey = relation.localKey || defaultLocalKey;
24
+ const baseRelationCondition = (root: TableDef, relation: RelationDef, rootAlias?: string): ExpressionNode => {
25
+ const rootTable = rootAlias || root.name;
26
+ const defaultLocalKey =
27
+ relation.type === RelationKinds.HasMany || relation.type === RelationKinds.HasOne
28
+ ? findPrimaryKey(root)
29
+ : findPrimaryKey(relation.target);
30
+ const localKey = relation.localKey || defaultLocalKey;
31
31
 
32
32
  switch (relation.type) {
33
- case RelationKinds.HasMany:
34
- case RelationKinds.HasOne:
35
- return eq(
36
- { type: 'Column', table: relation.target.name, name: relation.foreignKey },
37
- { type: 'Column', table: rootTable, name: localKey }
38
- );
39
- case RelationKinds.BelongsTo:
40
- return eq(
41
- { type: 'Column', table: relation.target.name, name: localKey },
42
- { type: 'Column', table: rootTable, name: relation.foreignKey }
43
- );
33
+ case RelationKinds.HasMany:
34
+ case RelationKinds.HasOne:
35
+ return eq(
36
+ { type: 'Column', table: relation.target.name, name: relation.foreignKey },
37
+ { type: 'Column', table: rootTable, name: localKey }
38
+ );
39
+ case RelationKinds.BelongsTo:
40
+ return eq(
41
+ { type: 'Column', table: relation.target.name, name: localKey },
42
+ { type: 'Column', table: rootTable, name: relation.foreignKey }
43
+ );
44
44
  case RelationKinds.BelongsToMany:
45
45
  throw new Error('BelongsToMany relations do not support the standard join condition builder');
46
46
  default:
@@ -50,25 +50,36 @@ const baseRelationCondition = (root: TableDef, relation: RelationDef, rootAlias?
50
50
 
51
51
  /**
52
52
  * Builds the join nodes required to include a BelongsToMany relation.
53
+ * @param root - The root table definition
54
+ * @param relationName - Name of the relation being joined
55
+ * @param relation - The BelongsToMany relation definition
56
+ * @param joinKind - The type of join to perform
57
+ * @param extra - Optional additional conditions for the target join
58
+ * @param rootAlias - Optional alias for the root table
59
+ * @returns Array of join nodes for the pivot and target tables
53
60
  */
54
- export const buildBelongsToManyJoins = (
55
- root: TableDef,
56
- relationName: string,
57
- relation: BelongsToManyRelation,
58
- joinKind: JoinKind,
59
- extra?: ExpressionNode,
60
- rootAlias?: string
61
- ): JoinNode[] => {
62
- const rootKey = relation.localKey || findPrimaryKey(root);
63
- const targetKey = relation.targetKey || findPrimaryKey(relation.target);
64
- const rootTable = rootAlias || root.name;
65
-
66
- const pivotCondition = eq(
67
- { type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToRoot },
68
- { type: 'Column', table: rootTable, name: rootKey }
69
- );
61
+ export const buildBelongsToManyJoins = (
62
+ root: TableDef,
63
+ relationName: string,
64
+ relation: BelongsToManyRelation,
65
+ joinKind: JoinKind,
66
+ extra?: ExpressionNode,
67
+ rootAlias?: string
68
+ ): JoinNode[] => {
69
+ const rootKey = relation.localKey || findPrimaryKey(root);
70
+ const targetKey = relation.targetKey || findPrimaryKey(relation.target);
71
+ const rootTable = rootAlias || root.name;
70
72
 
71
- const pivotJoin = createJoinNode(joinKind, relation.pivotTable.name, pivotCondition);
73
+ const pivotCondition = eq(
74
+ { type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToRoot },
75
+ { type: 'Column', table: rootTable, name: rootKey }
76
+ );
77
+
78
+ const pivotJoin = createJoinNode(
79
+ joinKind,
80
+ { type: 'Table', name: relation.pivotTable.name, schema: relation.pivotTable.schema },
81
+ pivotCondition
82
+ );
72
83
 
73
84
  let targetCondition: ExpressionNode = eq(
74
85
  { type: 'Column', table: relation.target.name, name: targetKey },
@@ -81,7 +92,7 @@ export const buildBelongsToManyJoins = (
81
92
 
82
93
  const targetJoin = createJoinNode(
83
94
  joinKind,
84
- relation.target.name,
95
+ { type: 'Table', name: relation.target.name, schema: relation.target.schema },
85
96
  targetCondition,
86
97
  relationName
87
98
  );
@@ -94,24 +105,26 @@ export const buildBelongsToManyJoins = (
94
105
  * @param root - Root table definition
95
106
  * @param relation - Relation definition
96
107
  * @param extra - Optional additional expression to combine with AND
108
+ * @param rootAlias - Optional alias for the root table
97
109
  * @returns Expression node representing the complete join condition
98
110
  */
99
- export const buildRelationJoinCondition = (
100
- root: TableDef,
101
- relation: RelationDef,
102
- extra?: ExpressionNode,
103
- rootAlias?: string
104
- ): ExpressionNode => {
105
- const base = baseRelationCondition(root, relation, rootAlias);
106
- return extra ? and(base, extra) : base;
107
- };
111
+ export const buildRelationJoinCondition = (
112
+ root: TableDef,
113
+ relation: RelationDef,
114
+ extra?: ExpressionNode,
115
+ rootAlias?: string
116
+ ): ExpressionNode => {
117
+ const base = baseRelationCondition(root, relation, rootAlias);
118
+ return extra ? and(base, extra) : base;
119
+ };
108
120
 
109
121
  /**
110
122
  * Builds a relation correlation condition for subqueries
111
123
  * @param root - Root table definition
112
124
  * @param relation - Relation definition
125
+ * @param rootAlias - Optional alias for the root table
113
126
  * @returns Expression node representing the correlation condition
114
127
  */
115
- export const buildRelationCorrelation = (root: TableDef, relation: RelationDef, rootAlias?: string): ExpressionNode => {
116
- return baseRelationCondition(root, relation, rootAlias);
117
- };
128
+ export const buildRelationCorrelation = (root: TableDef, relation: RelationDef, rootAlias?: string): ExpressionNode => {
129
+ return baseRelationCondition(root, relation, rootAlias);
130
+ };
@@ -20,8 +20,8 @@ import {
20
20
  } from './relation-conditions.js';
21
21
  import { JoinKind, JOIN_KINDS } from '../core/sql/sql.js';
22
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';
23
+ import { createJoinNode } from '../core/ast/join-node.js';
24
+ import { getJoinRelationName } from '../core/ast/join-metadata.js';
25
25
  import { makeRelationAlias } from './relation-alias.js';
26
26
  import { buildDefaultPivotColumns } from './relation-utils.js';
27
27
 
@@ -70,17 +70,17 @@ export class RelationService {
70
70
  * @param predicate - Optional predicate expression
71
71
  * @returns Relation result with updated state and hydration
72
72
  */
73
- match(
74
- relationName: string,
75
- predicate?: ExpressionNode
76
- ): RelationResult {
77
- const joined = this.joinRelation(relationName, JOIN_KINDS.INNER, predicate);
78
- const pk = findPrimaryKey(this.table);
79
- const distinctCols: ColumnNode[] = [{ type: 'Column', table: this.rootTableName(), name: pk }];
80
- const existingDistinct = joined.state.ast.distinct ? joined.state.ast.distinct : [];
81
- const nextState = this.astService(joined.state).withDistinct([...existingDistinct, ...distinctCols]);
82
- return { state: nextState, hydration: joined.hydration };
83
- }
73
+ match(
74
+ relationName: string,
75
+ predicate?: ExpressionNode
76
+ ): RelationResult {
77
+ const joined = this.joinRelation(relationName, JOIN_KINDS.INNER, predicate);
78
+ const pk = findPrimaryKey(this.table);
79
+ const distinctCols: ColumnNode[] = [{ type: 'Column', table: this.rootTableName(), name: pk }];
80
+ const existingDistinct = joined.state.ast.distinct ? joined.state.ast.distinct : [];
81
+ const nextState = this.astService(joined.state).withDistinct([...existingDistinct, ...distinctCols]);
82
+ return { state: nextState, hydration: joined.hydration };
83
+ }
84
84
 
85
85
  /**
86
86
  * Includes a relation in the query result
@@ -94,7 +94,7 @@ export class RelationService {
94
94
 
95
95
  const relation = this.getRelation(relationName);
96
96
  const aliasPrefix = options?.aliasPrefix ?? relationName;
97
- const alreadyJoined = state.ast.joins.some(j => getJoinRelationName(j) === relationName);
97
+ const alreadyJoined = state.ast.joins.some(j => getJoinRelationName(j) === relationName);
98
98
 
99
99
  if (!alreadyJoined) {
100
100
  const joined = this.joinRelation(relationName, options?.joinKind ?? JOIN_KINDS.LEFT, options?.filter);
@@ -114,7 +114,7 @@ export class RelationService {
114
114
  prefix: string,
115
115
  keys: string[],
116
116
  missingMsg: (col: string) => string
117
- ) : Record<string, ColumnDef> => {
117
+ ): Record<string, ColumnDef> => {
118
118
  return keys.reduce((acc, key) => {
119
119
  const def = columns[key];
120
120
  if (!def) {
@@ -190,22 +190,22 @@ export class RelationService {
190
190
  * @param ast - Query AST to modify
191
191
  * @returns Modified query AST with relation correlation
192
192
  */
193
- applyRelationCorrelation(
194
- relationName: string,
195
- ast: SelectQueryNode,
196
- additionalCorrelation?: ExpressionNode
197
- ): SelectQueryNode {
198
- const relation = this.getRelation(relationName);
199
- const rootAlias = this.state.ast.from.type === 'Table' ? this.state.ast.from.alias : undefined;
200
- let correlation = buildRelationCorrelation(this.table, relation, rootAlias);
201
- if (additionalCorrelation) {
202
- correlation = and(correlation, additionalCorrelation);
203
- }
204
- const whereInSubquery = ast.where
205
- ? and(correlation, ast.where)
206
- : correlation;
207
-
208
- return {
193
+ applyRelationCorrelation(
194
+ relationName: string,
195
+ ast: SelectQueryNode,
196
+ additionalCorrelation?: ExpressionNode
197
+ ): SelectQueryNode {
198
+ const relation = this.getRelation(relationName);
199
+ const rootAlias = this.state.ast.from.type === 'Table' ? this.state.ast.from.alias : undefined;
200
+ let correlation = buildRelationCorrelation(this.table, relation, rootAlias);
201
+ if (additionalCorrelation) {
202
+ correlation = and(correlation, additionalCorrelation);
203
+ }
204
+ const whereInSubquery = ast.where
205
+ ? and(correlation, ast.where)
206
+ : correlation;
207
+
208
+ return {
209
209
  ...ast,
210
210
  where: whereInSubquery
211
211
  };
@@ -219,28 +219,33 @@ export class RelationService {
219
219
  * @param extraCondition - Additional join condition
220
220
  * @returns Updated query state with join
221
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(joinKind, relation.target.name, condition, relationName);
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
+ );
244
249
 
245
250
  return this.astService(state).withJoin(joinNode);
246
251
  }
@@ -284,15 +289,15 @@ export class RelationService {
284
289
  * @param state - Current query state
285
290
  * @returns QueryAstService instance
286
291
  */
287
- private astService(state: SelectQueryState = this.state): QueryAstService {
288
- return this.createQueryAstService(this.table, state);
289
- }
290
-
291
- private rootTableName(): string {
292
- const from = this.state.ast.from;
293
- if (from.type === 'Table' && from.alias) return from.alias;
294
- return this.table.name;
295
- }
296
- }
292
+ private astService(state: SelectQueryState = this.state): QueryAstService {
293
+ return this.createQueryAstService(this.table, state);
294
+ }
295
+
296
+ private rootTableName(): string {
297
+ const from = this.state.ast.from;
298
+ if (from.type === 'Table' && from.alias) return from.alias;
299
+ return this.table.name;
300
+ }
301
+ }
297
302
 
298
303
  export type { RelationResult } from './relation-projection-helper.js';
@@ -2,6 +2,9 @@ import { BelongsToManyRelation } from '../schema/relation.js';
2
2
 
3
3
  /**
4
4
  * Builds a default set of pivot columns, excluding keys used for joins.
5
+ * @param rel - The BelongsToMany relation definition
6
+ * @param pivotPk - The primary key column name of the pivot table
7
+ * @returns Array of column names that can be included in pivot table selections
5
8
  */
6
9
  export const buildDefaultPivotColumns = (
7
10
  rel: BelongsToManyRelation,
@@ -0,0 +1,40 @@
1
+ import { SelectQueryNode } from '../../core/ast/query.js';
2
+ import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps.js';
3
+ import { QueryAstService } from '../query-ast-service.js';
4
+ import { SelectQueryState } from '../select-query-state.js';
5
+
6
+ /**
7
+ * Facet responsible for Common Table Expressions (WITH clauses)
8
+ */
9
+ export class SelectCTEFacet {
10
+ /**
11
+ * Creates a new SelectCTEFacet instance
12
+ * @param env - Query builder environment
13
+ * @param createAstService - Function to create AST service
14
+ */
15
+ constructor(
16
+ private readonly env: SelectQueryBuilderEnvironment,
17
+ private readonly createAstService: (state: SelectQueryState) => QueryAstService
18
+ ) { }
19
+
20
+ /**
21
+ * Adds a Common Table Expression to the query
22
+ * @param context - Current query context
23
+ * @param name - CTE name
24
+ * @param subAst - CTE query AST
25
+ * @param columns - Optional column names
26
+ * @param recursive - Whether the CTE is recursive
27
+ * @returns Updated query context with CTE
28
+ */
29
+ withCTE(
30
+ context: SelectQueryBuilderContext,
31
+ name: string,
32
+ subAst: SelectQueryNode,
33
+ columns: string[] | undefined,
34
+ recursive: boolean
35
+ ): SelectQueryBuilderContext {
36
+ const astService = this.createAstService(context.state);
37
+ const nextState = astService.withCte(name, subAst, columns, recursive);
38
+ return { state: nextState, hydration: context.hydration };
39
+ }
40
+ }
@@ -0,0 +1,80 @@
1
+ import { SelectQueryNode } from '../../core/ast/query.js';
2
+ import { OperandNode } from '../../core/ast/expression.js';
3
+ import { derivedTable, fnTable } from '../../core/ast/builders.js';
4
+ import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps.js';
5
+ import { QueryAstService } from '../query-ast-service.js';
6
+ import { SelectQueryState } from '../select-query-state.js';
7
+
8
+ /**
9
+ * Facet responsible for FROM clause operations
10
+ */
11
+ export class SelectFromFacet {
12
+ /**
13
+ * Creates a new SelectFromFacet instance
14
+ * @param env - Query builder environment
15
+ * @param createAstService - Function to create AST service
16
+ */
17
+ constructor(
18
+ private readonly env: SelectQueryBuilderEnvironment,
19
+ private readonly createAstService: (state: SelectQueryState) => QueryAstService
20
+ ) { }
21
+
22
+ /**
23
+ * Applies an alias to the FROM table
24
+ * @param context - Current query context
25
+ * @param alias - Alias to apply
26
+ * @returns Updated query context with aliased FROM
27
+ */
28
+ as(context: SelectQueryBuilderContext, alias: string): SelectQueryBuilderContext {
29
+ const from = context.state.ast.from;
30
+ if (from.type !== 'Table') {
31
+ throw new Error('Cannot alias non-table FROM sources');
32
+ }
33
+ const nextFrom = { ...from, alias };
34
+ const astService = this.createAstService(context.state);
35
+ const nextState = astService.withFrom(nextFrom);
36
+ return { state: nextState, hydration: context.hydration };
37
+ }
38
+
39
+ /**
40
+ * Sets the FROM clause to a subquery
41
+ * @param context - Current query context
42
+ * @param subAst - Subquery AST
43
+ * @param alias - Alias for the subquery
44
+ * @param columnAliases - Optional column aliases
45
+ * @returns Updated query context with subquery FROM
46
+ */
47
+ fromSubquery(
48
+ context: SelectQueryBuilderContext,
49
+ subAst: SelectQueryNode,
50
+ alias: string,
51
+ columnAliases?: string[]
52
+ ): SelectQueryBuilderContext {
53
+ const fromNode = derivedTable(subAst, alias, columnAliases);
54
+ const astService = this.createAstService(context.state);
55
+ const nextState = astService.withFrom(fromNode);
56
+ return { state: nextState, hydration: context.hydration };
57
+ }
58
+
59
+ /**
60
+ * Sets the FROM clause to a function table
61
+ * @param context - Current query context
62
+ * @param name - Function name
63
+ * @param args - Function arguments
64
+ * @param alias - Optional alias for the function table
65
+ * @param options - Optional function table options
66
+ * @returns Updated query context with function table FROM
67
+ */
68
+ fromFunctionTable(
69
+ context: SelectQueryBuilderContext,
70
+ name: string,
71
+ args: OperandNode[],
72
+ alias?: string,
73
+ options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
74
+ ): SelectQueryBuilderContext {
75
+ const functionTable = fnTable(name, args, alias, options);
76
+ const astService = this.createAstService(context.state);
77
+ const nextState = astService.withFrom(functionTable);
78
+ return { state: nextState, hydration: context.hydration };
79
+ }
80
+ }
@@ -0,0 +1,62 @@
1
+ import { TableDef } from '../../schema/table.js';
2
+ import { BinaryExpressionNode } from '../../core/ast/expression.js';
3
+ import { SelectQueryNode } from '../../core/ast/query.js';
4
+ import { JoinKind } from '../../core/sql/sql.js';
5
+ import { derivedTable, fnTable } from '../../core/ast/builders.js';
6
+ import { createJoinNode } from '../../core/ast/join-node.js';
7
+ import { SelectQueryBuilderContext, SelectQueryBuilderEnvironment } from '../select-query-builder-deps.js';
8
+ import { QueryAstService } from '../query-ast-service.js';
9
+ import { OperandNode } from '../../core/ast/expression.js';
10
+ import { SelectQueryState } from '../select-query-state.js';
11
+
12
+ /**
13
+ * Facet responsible for JOIN operations
14
+ */
15
+ export class SelectJoinFacet {
16
+ constructor(
17
+ private readonly env: SelectQueryBuilderEnvironment,
18
+ private readonly createAstService: (state: SelectQueryState) => QueryAstService
19
+ ) { }
20
+
21
+ applyJoin(
22
+ context: SelectQueryBuilderContext,
23
+ table: TableDef,
24
+ condition: BinaryExpressionNode,
25
+ kind: JoinKind
26
+ ): SelectQueryBuilderContext {
27
+ const joinNode = createJoinNode(kind, { type: 'Table', name: table.name, schema: table.schema }, condition);
28
+ const astService = this.createAstService(context.state);
29
+ const nextState = astService.withJoin(joinNode);
30
+ return { state: nextState, hydration: context.hydration };
31
+ }
32
+
33
+ joinSubquery(
34
+ context: SelectQueryBuilderContext,
35
+ subAst: SelectQueryNode,
36
+ alias: string,
37
+ condition: BinaryExpressionNode,
38
+ joinKind: JoinKind,
39
+ columnAliases?: string[]
40
+ ): SelectQueryBuilderContext {
41
+ const joinNode = createJoinNode(joinKind, derivedTable(subAst, alias, columnAliases), condition);
42
+ const astService = this.createAstService(context.state);
43
+ const nextState = astService.withJoin(joinNode);
44
+ return { state: nextState, hydration: context.hydration };
45
+ }
46
+
47
+ joinFunctionTable(
48
+ context: SelectQueryBuilderContext,
49
+ name: string,
50
+ args: OperandNode[],
51
+ alias: string,
52
+ condition: BinaryExpressionNode,
53
+ joinKind: JoinKind,
54
+ options?: { lateral?: boolean; withOrdinality?: boolean; columnAliases?: string[]; schema?: string }
55
+ ): SelectQueryBuilderContext {
56
+ const functionTable = fnTable(name, args, alias, options);
57
+ const joinNode = createJoinNode(joinKind, functionTable, condition);
58
+ const astService = this.createAstService(context.state);
59
+ const nextState = astService.withJoin(joinNode);
60
+ return { state: nextState, hydration: context.hydration };
61
+ }
62
+ }