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,310 +1,5 @@
1
- import { TableDef } from '../schema/table.js';
2
- import { BelongsToManyRelation, HasManyRelation, HasOneRelation, BelongsToRelation } from '../schema/relation.js';
3
- import { SelectQueryBuilder } from '../query-builder/select.js';
4
- import { inList, LiteralNode } from '../core/ast/expression.js';
5
- import { EntityContext } from './entity-context.js';
6
- import type { QueryResult } from '../core/execution/db-executor.js';
7
- import { ColumnDef } from '../schema/column-types.js';
8
- import { findPrimaryKey } from '../query-builder/hydration-planner.js';
9
-
10
- /**
11
- * An array of database rows, each represented as a record of string keys to unknown values.
12
- */
13
- type Rows = Record<string, unknown>[];
14
-
15
- /**
16
- * Represents a single tracked entity from the EntityContext for a table.
17
- */
18
- type EntityTracker = ReturnType<EntityContext['getEntitiesForTable']>[number];
19
-
20
- /**
21
- * Creates a record of all columns from the given table definition.
22
- * @param table - The table definition to select columns from.
23
- * @returns A record mapping column names to their definitions.
24
- */
25
- const selectAllColumns = (table: TableDef): Record<string, ColumnDef> =>
26
- Object.entries(table.columns).reduce((acc, [name, def]) => {
27
- acc[name] = def;
28
- return acc;
29
- }, {} as Record<string, ColumnDef>);
30
-
31
- /**
32
- * Extracts rows from query results into a standardized format.
33
- * @param results - The query results to process.
34
- * @returns An array of rows as records.
35
- */
36
- const rowsFromResults = (results: QueryResult[]): Rows => {
37
- const rows: Rows = [];
38
- for (const result of results) {
39
- const { columns, values } = result;
40
- for (const valueRow of values) {
41
- const row: Record<string, unknown> = {};
42
- columns.forEach((column, idx) => {
43
- row[column] = valueRow[idx];
44
- });
45
- rows.push(row);
46
- }
47
- }
48
- return rows;
49
- };
50
-
51
- /**
52
- * Executes a select query and returns the resulting rows.
53
- * @param ctx - The entity context for execution.
54
- * @param qb - The select query builder.
55
- * @returns A promise resolving to the rows from the query.
56
- */
57
- const executeQuery = async (ctx: EntityContext, qb: SelectQueryBuilder<unknown, TableDef>): Promise<Rows> => {
58
- const compiled = ctx.dialect.compileSelect(qb.getAST());
59
- const results = await ctx.executor.executeSql(compiled.sql, compiled.params);
60
- return rowsFromResults(results);
61
- };
62
-
63
- /**
64
- * Converts a value to a string key, handling null and undefined as empty string.
65
- * @param value - The value to convert.
66
- * @returns The string representation of the value.
67
- */
68
- const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
69
-
70
- /**
71
- * Collects unique keys from the root entities based on the specified key property.
72
- * @param roots - The tracked entities to collect keys from.
73
- * @param key - The property name to use as the key.
74
- * @returns A set of unique key values.
75
- */
76
- const collectKeysFromRoots = (roots: EntityTracker[], key: string): Set<unknown> => {
77
- const collected = new Set<unknown>();
78
- for (const tracked of roots) {
79
- const value = tracked.entity[key];
80
- if (value !== null && value !== undefined) {
81
- collected.add(value);
82
- }
83
- }
84
- return collected;
85
- };
86
-
87
- /**
88
- * Builds an array of values suitable for an IN list expression from a set of keys.
89
- * @param keys - The set of keys to convert.
90
- * @returns An array of string, number, or LiteralNode values.
91
- */
92
- const buildInListValues = (keys: Set<unknown>): (string | number | LiteralNode)[] =>
93
- Array.from(keys) as (string | number | LiteralNode)[];
94
-
95
- /**
96
- * Fetches rows from a table where the specified column matches any of the given keys.
97
- * @param ctx - The entity context.
98
- * @param table - The target table.
99
- * @param column - The column to match against.
100
- * @param keys - The set of keys to match.
101
- * @returns A promise resolving to the matching rows.
102
- */
103
- const fetchRowsForKeys = async (
104
- ctx: EntityContext,
105
- table: TableDef,
106
- column: ColumnDef,
107
- keys: Set<unknown>
108
- ): Promise<Rows> => {
109
- const qb = new SelectQueryBuilder(table).select(selectAllColumns(table));
110
- qb.where(inList(column, buildInListValues(keys)));
111
- return executeQuery(ctx, qb);
112
- };
113
-
114
- /**
115
- * Groups rows by the value of a key column, allowing multiple rows per key.
116
- * @param rows - The rows to group.
117
- * @param keyColumn - The column name to group by.
118
- * @returns A map from key strings to arrays of rows.
119
- */
120
- const groupRowsByMany = (rows: Rows, keyColumn: string): Map<string, Rows> => {
121
- const grouped = new Map<string, Rows>();
122
- for (const row of rows) {
123
- const value = row[keyColumn];
124
- if (value === null || value === undefined) continue;
125
- const key = toKey(value);
126
- const bucket = grouped.get(key) ?? [];
127
- bucket.push(row);
128
- grouped.set(key, bucket);
129
- }
130
- return grouped;
131
- };
132
-
133
- /**
134
- * Groups rows by the value of a key column, keeping only one row per key.
135
- * @param rows - The rows to group.
136
- * @param keyColumn - The column name to group by.
137
- * @returns A map from key strings to single rows.
138
- */
139
- const groupRowsByUnique = (rows: Rows, keyColumn: string): Map<string, Record<string, unknown>> => {
140
- const lookup = new Map<string, Record<string, unknown>>();
141
- for (const row of rows) {
142
- const value = row[keyColumn];
143
- if (value === null || value === undefined) continue;
144
- const key = toKey(value);
145
- if (!lookup.has(key)) {
146
- lookup.set(key, row);
147
- }
148
- }
149
- return lookup;
150
- };
151
-
152
- /**
153
- * Loads related entities for a has-many relation in batch.
154
- * @param ctx - The entity context.
155
- * @param rootTable - The root table of the relation.
156
- * @param _relationName - The name of the relation (unused).
157
- * @param relation - The has-many relation definition.
158
- * @returns A promise resolving to a map of root keys to arrays of related rows.
159
- */
160
- export const loadHasManyRelation = async (
161
- ctx: EntityContext,
162
- rootTable: TableDef,
163
- _relationName: string,
164
- relation: HasManyRelation
165
- ): Promise<Map<string, Rows>> => {
166
- const localKey = relation.localKey || findPrimaryKey(rootTable);
167
- const roots = ctx.getEntitiesForTable(rootTable);
168
- const keys = collectKeysFromRoots(roots, localKey);
169
-
170
- if (!keys.size) {
171
- return new Map();
172
- }
173
-
174
- const fkColumn = relation.target.columns[relation.foreignKey];
175
- if (!fkColumn) return new Map();
176
-
177
- const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys);
178
- return groupRowsByMany(rows, relation.foreignKey);
179
- };
180
-
181
- /**
182
- * Loads related entities for a has-one relation in batch.
183
- * @param ctx - The entity context.
184
- * @param rootTable - The root table of the relation.
185
- * @param _relationName - The name of the relation (unused).
186
- * @param relation - The has-one relation definition.
187
- * @returns A promise resolving to a map of root keys to single related rows.
188
- */
189
- export const loadHasOneRelation = async (
190
- ctx: EntityContext,
191
- rootTable: TableDef,
192
- _relationName: string,
193
- relation: HasOneRelation
194
- ): Promise<Map<string, Record<string, unknown>>> => {
195
- const localKey = relation.localKey || findPrimaryKey(rootTable);
196
- const roots = ctx.getEntitiesForTable(rootTable);
197
- const keys = collectKeysFromRoots(roots, localKey);
198
-
199
- if (!keys.size) {
200
- return new Map();
201
- }
202
-
203
- const fkColumn = relation.target.columns[relation.foreignKey];
204
- if (!fkColumn) return new Map();
205
-
206
- const rows = await fetchRowsForKeys(ctx, relation.target, fkColumn, keys);
207
- return groupRowsByUnique(rows, relation.foreignKey);
208
- };
209
-
210
- /**
211
- * Loads related entities for a belongs-to relation in batch.
212
- * @param ctx - The entity context.
213
- * @param rootTable - The root table of the relation.
214
- * @param _relationName - The name of the relation (unused).
215
- * @param relation - The belongs-to relation definition.
216
- * @returns A promise resolving to a map of foreign keys to single related rows.
217
- */
218
- export const loadBelongsToRelation = async (
219
- ctx: EntityContext,
220
- rootTable: TableDef,
221
- _relationName: string,
222
- relation: BelongsToRelation
223
- ): Promise<Map<string, Record<string, unknown>>> => {
224
- const roots = ctx.getEntitiesForTable(rootTable);
225
- const foreignKeys = collectKeysFromRoots(roots, relation.foreignKey);
226
-
227
- if (!foreignKeys.size) {
228
- return new Map();
229
- }
230
-
231
- const targetKey = relation.localKey || findPrimaryKey(relation.target);
232
- const pkColumn = relation.target.columns[targetKey];
233
- if (!pkColumn) return new Map();
234
-
235
- const rows = await fetchRowsForKeys(ctx, relation.target, pkColumn, foreignKeys);
236
- return groupRowsByUnique(rows, targetKey);
237
- };
238
-
239
- /**
240
- * Loads related entities for a belongs-to-many relation in batch, including pivot data.
241
- * @param ctx - The entity context.
242
- * @param rootTable - The root table of the relation.
243
- * @param _relationName - The name of the relation (unused).
244
- * @param relation - The belongs-to-many relation definition.
245
- * @returns A promise resolving to a map of root keys to arrays of related rows with pivot data.
246
- */
247
- export const loadBelongsToManyRelation = async (
248
- ctx: EntityContext,
249
- rootTable: TableDef,
250
- _relationName: string,
251
- relation: BelongsToManyRelation
252
- ): Promise<Map<string, Rows>> => {
253
- const rootKey = relation.localKey || findPrimaryKey(rootTable);
254
- const roots = ctx.getEntitiesForTable(rootTable);
255
- const rootIds = collectKeysFromRoots(roots, rootKey);
256
-
257
- if (!rootIds.size) {
258
- return new Map();
259
- }
260
-
261
- const pivotColumn = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
262
- if (!pivotColumn) return new Map();
263
-
264
- const pivotRows = await fetchRowsForKeys(ctx, relation.pivotTable, pivotColumn, rootIds);
265
- const rootLookup = new Map<string, { targetId: unknown; pivot: Record<string, unknown> }[]>();
266
- const targetIds = new Set<unknown>();
267
-
268
- for (const pivot of pivotRows) {
269
- const rootValue = pivot[relation.pivotForeignKeyToRoot];
270
- const targetValue = pivot[relation.pivotForeignKeyToTarget];
271
- if (rootValue === null || rootValue === undefined || targetValue === null || targetValue === undefined) {
272
- continue;
273
- }
274
- const bucket = rootLookup.get(toKey(rootValue)) ?? [];
275
- bucket.push({
276
- targetId: targetValue,
277
- pivot: { ...pivot }
278
- });
279
- rootLookup.set(toKey(rootValue), bucket);
280
- targetIds.add(targetValue);
281
- }
282
-
283
- if (!targetIds.size) {
284
- return new Map();
285
- }
286
-
287
- const targetKey = relation.targetKey || findPrimaryKey(relation.target);
288
- const targetPkColumn = relation.target.columns[targetKey];
289
- if (!targetPkColumn) return new Map();
290
-
291
- const targetRows = await fetchRowsForKeys(ctx, relation.target, targetPkColumn, targetIds);
292
- const targetMap = groupRowsByUnique(targetRows, targetKey);
293
- const result = new Map<string, Rows>();
294
-
295
- for (const [rootId, entries] of rootLookup.entries()) {
296
- const bucket: Rows = [];
297
- for (const entry of entries) {
298
- const targetRow = targetMap.get(toKey(entry.targetId));
299
- if (!targetRow) continue;
300
- bucket.push({
301
- ...targetRow,
302
- _pivot: entry.pivot
303
- });
304
- }
305
- result.set(rootId, bucket);
306
- }
307
-
308
- return result;
309
- };
1
+ export { loadHasManyRelation } from './lazy-batch/has-many.js';
2
+ export { loadHasOneRelation } from './lazy-batch/has-one.js';
3
+ export { loadBelongsToRelation } from './lazy-batch/belongs-to.js';
4
+ export { loadBelongsToManyRelation } from './lazy-batch/belongs-to-many.js';
310
5
 
@@ -1,4 +1,4 @@
1
- import { BelongsToReference } from '../../schema/types.js';
1
+ import { BelongsToReferenceApi } from '../../schema/types.js';
2
2
  import { EntityContext } from '../entity-context.js';
3
3
  import { RelationKey } from '../runtime-types.js';
4
4
  import { BelongsToRelation } from '../../schema/relation.js';
@@ -26,7 +26,7 @@ const hideInternal = (obj: object, keys: string[]): void => {
26
26
  *
27
27
  * @template TParent The type of the parent entity.
28
28
  */
29
- export class DefaultBelongsToReference<TParent> implements BelongsToReference<TParent> {
29
+ export class DefaultBelongsToReference<TParent extends object> implements BelongsToReferenceApi<TParent> {
30
30
  private loaded = false;
31
31
  private current: TParent | null = null;
32
32
 
@@ -69,15 +69,29 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
69
69
  this.items = rows.map(row => this.createEntity(row));
70
70
  this.loaded = true;
71
71
  return this.items;
72
- }
73
-
74
- /**
75
- * Gets the current items in the collection.
76
- * @returns Array of child entities
77
- */
78
- getItems(): TChild[] {
79
- return this.items;
80
- }
72
+ }
73
+
74
+ /**
75
+ * Gets the current items in the collection.
76
+ * @returns Array of child entities
77
+ */
78
+ getItems(): TChild[] {
79
+ return this.items;
80
+ }
81
+
82
+ /**
83
+ * Array-compatible length for testing frameworks.
84
+ */
85
+ get length(): number {
86
+ return this.items.length;
87
+ }
88
+
89
+ /**
90
+ * Enables iteration over the collection like an array.
91
+ */
92
+ [Symbol.iterator](): Iterator<TChild> {
93
+ return this.items[Symbol.iterator]();
94
+ }
81
95
 
82
96
  /**
83
97
  * Adds a new child entity to the collection.
@@ -1,4 +1,4 @@
1
- import { HasOneReference } from '../../schema/types.js';
1
+ import { HasOneReferenceApi } from '../../schema/types.js';
2
2
  import { EntityContext } from '../entity-context.js';
3
3
  import { RelationKey } from '../runtime-types.js';
4
4
  import { HasOneRelation } from '../../schema/relation.js';
@@ -26,7 +26,7 @@ const hideInternal = (obj: object, keys: string[]): void => {
26
26
  *
27
27
  * @template TChild The type of the child entity.
28
28
  */
29
- export class DefaultHasOneReference<TChild> implements HasOneReference<TChild> {
29
+ export class DefaultHasOneReference<TChild extends object> implements HasOneReferenceApi<TChild> {
30
30
  private loaded = false;
31
31
  private current: TChild | null = null;
32
32
 
@@ -28,7 +28,8 @@ const hideInternal = (obj: object, keys: string[]): void => {
28
28
  *
29
29
  * @template TTarget The type of the target entities in the collection.
30
30
  */
31
- export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollection<TTarget> {
31
+ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefined = undefined>
32
+ implements ManyToManyCollection<TTarget, TPivot> {
32
33
  private loaded = false;
33
34
  private items: TTarget[] = [];
34
35
 
@@ -78,19 +79,33 @@ export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollectio
78
79
  return this.items;
79
80
  }
80
81
 
81
- /**
82
- * Returns the currently loaded items.
83
- * @returns Array of target entities.
84
- */
85
- getItems(): TTarget[] {
86
- return this.items;
87
- }
88
-
89
- /**
90
- * Attaches an entity to the collection.
91
- * Registers an 'attach' change in the entity context.
92
- * @param target Entity instance or its primary key value.
93
- */
82
+ /**
83
+ * Returns the currently loaded items.
84
+ * @returns Array of target entities.
85
+ */
86
+ getItems(): TTarget[] {
87
+ return this.items;
88
+ }
89
+
90
+ /**
91
+ * Array-compatible length for testing frameworks.
92
+ */
93
+ get length(): number {
94
+ return this.items.length;
95
+ }
96
+
97
+ /**
98
+ * Enables iteration over the collection like an array.
99
+ */
100
+ [Symbol.iterator](): Iterator<TTarget> {
101
+ return this.items[Symbol.iterator]();
102
+ }
103
+
104
+ /**
105
+ * Attaches an entity to the collection.
106
+ * Registers an 'attach' change in the entity context.
107
+ * @param target Entity instance or its primary key value.
108
+ */
94
109
  attach(target: TTarget | number | string): void {
95
110
  const entity = this.ensureEntity(target);
96
111
  const id = this.extractId(entity);
@@ -10,8 +10,8 @@ type AnyFn = (...args: unknown[]) => unknown;
10
10
 
11
11
  type RelationWrapper =
12
12
  | HasManyCollection<unknown>
13
- | HasOneReference<unknown>
14
- | BelongsToReference<unknown>
13
+ | HasOneReference
14
+ | BelongsToReference
15
15
  | ManyToManyCollection<unknown>;
16
16
 
17
17
  type FunctionKeys<T> = {
@@ -184,15 +184,15 @@ const handleHasMany = async (
184
184
  }
185
185
  };
186
186
 
187
- const handleHasOne = async (
188
- session: OrmSession,
189
- root: AnyEntity,
190
- relationName: string,
191
- relation: HasOneRelation,
192
- payload: unknown,
193
- options: SaveGraphOptions
194
- ): Promise<void> => {
195
- const ref = root[relationName] as unknown as HasOneReference<unknown>;
187
+ const handleHasOne = async (
188
+ session: OrmSession,
189
+ root: AnyEntity,
190
+ relationName: string,
191
+ relation: HasOneRelation,
192
+ payload: unknown,
193
+ options: SaveGraphOptions
194
+ ): Promise<void> => {
195
+ const ref = root[relationName] as unknown as HasOneReference<object>;
196
196
  if (payload === undefined) return;
197
197
  if (payload === null) {
198
198
  ref.set(null);
@@ -212,15 +212,15 @@ const handleHasOne = async (
212
212
  }
213
213
  };
214
214
 
215
- const handleBelongsTo = async (
216
- session: OrmSession,
217
- root: AnyEntity,
218
- relationName: string,
219
- relation: BelongsToRelation,
220
- payload: unknown,
221
- options: SaveGraphOptions
222
- ): Promise<void> => {
223
- const ref = root[relationName] as unknown as BelongsToReference<unknown>;
215
+ const handleBelongsTo = async (
216
+ session: OrmSession,
217
+ root: AnyEntity,
218
+ relationName: string,
219
+ relation: BelongsToRelation,
220
+ payload: unknown,
221
+ options: SaveGraphOptions
222
+ ): Promise<void> => {
223
+ const ref = root[relationName] as unknown as BelongsToReference<object>;
224
224
  if (payload === undefined) return;
225
225
  if (payload === null) {
226
226
  ref.set(null);
@@ -1,10 +1,11 @@
1
- import { TableDef } from '../schema/table.js';
2
- import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation.js';
3
- import { ExpressionNode, eq, and } from '../core/ast/expression.js';
4
- import { findPrimaryKey } from './hydration-planner.js';
5
- import { JoinNode } from '../core/ast/join.js';
6
- import { JoinKind } from '../core/sql/sql.js';
7
- import { createJoinNode } from '../core/ast/join-node.js';
1
+ import { TableDef } from '../schema/table.js';
2
+ import { RelationDef, RelationKinds, BelongsToManyRelation } from '../schema/relation.js';
3
+ import { ExpressionNode, eq, and } from '../core/ast/expression.js';
4
+ import { TableSourceNode } from '../core/ast/query.js';
5
+ import { findPrimaryKey } from './hydration-planner.js';
6
+ import { JoinNode } from '../core/ast/join.js';
7
+ import { JoinKind } from '../core/sql/sql.js';
8
+ import { createJoinNode } from '../core/ast/join-node.js';
8
9
 
9
10
  /**
10
11
  * Utility function to handle unreachable code paths
@@ -21,26 +22,32 @@ const assertNever = (value: never): never => {
21
22
  * @param relation - Relation definition
22
23
  * @returns Expression node representing the join condition
23
24
  */
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);
25
+ const baseRelationCondition = (
26
+ root: TableDef,
27
+ relation: RelationDef,
28
+ rootAlias?: string,
29
+ targetTableName?: string
30
+ ): ExpressionNode => {
31
+ const rootTable = rootAlias || root.name;
32
+ const targetTable = targetTableName ?? relation.target.name;
33
+ const defaultLocalKey =
34
+ relation.type === RelationKinds.HasMany || relation.type === RelationKinds.HasOne
35
+ ? findPrimaryKey(root)
36
+ : findPrimaryKey(relation.target);
30
37
  const localKey = relation.localKey || defaultLocalKey;
31
38
 
32
39
  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
- );
40
+ case RelationKinds.HasMany:
41
+ case RelationKinds.HasOne:
42
+ return eq(
43
+ { type: 'Column', table: targetTable, name: relation.foreignKey },
44
+ { type: 'Column', table: rootTable, name: localKey }
45
+ );
46
+ case RelationKinds.BelongsTo:
47
+ return eq(
48
+ { type: 'Column', table: targetTable, name: localKey },
49
+ { type: 'Column', table: rootTable, name: relation.foreignKey }
50
+ );
44
51
  case RelationKinds.BelongsToMany:
45
52
  throw new Error('BelongsToMany relations do not support the standard join condition builder');
46
53
  default:
@@ -58,14 +65,16 @@ const baseRelationCondition = (root: TableDef, relation: RelationDef, rootAlias?
58
65
  * @param rootAlias - Optional alias for the root table
59
66
  * @returns Array of join nodes for the pivot and target tables
60
67
  */
61
- export const buildBelongsToManyJoins = (
62
- root: TableDef,
63
- relationName: string,
64
- relation: BelongsToManyRelation,
65
- joinKind: JoinKind,
66
- extra?: ExpressionNode,
67
- rootAlias?: string
68
- ): JoinNode[] => {
68
+ export const buildBelongsToManyJoins = (
69
+ root: TableDef,
70
+ relationName: string,
71
+ relation: BelongsToManyRelation,
72
+ joinKind: JoinKind,
73
+ extra?: ExpressionNode,
74
+ rootAlias?: string,
75
+ targetTable?: TableSourceNode,
76
+ targetTableName?: string
77
+ ): JoinNode[] => {
69
78
  const rootKey = relation.localKey || findPrimaryKey(root);
70
79
  const targetKey = relation.targetKey || findPrimaryKey(relation.target);
71
80
  const rootTable = rootAlias || root.name;
@@ -81,21 +90,27 @@ export const buildBelongsToManyJoins = (
81
90
  pivotCondition
82
91
  );
83
92
 
84
- let targetCondition: ExpressionNode = eq(
85
- { type: 'Column', table: relation.target.name, name: targetKey },
86
- { type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToTarget }
87
- );
88
-
89
- if (extra) {
90
- targetCondition = and(targetCondition, extra);
91
- }
92
-
93
- const targetJoin = createJoinNode(
94
- joinKind,
95
- { type: 'Table', name: relation.target.name, schema: relation.target.schema },
96
- targetCondition,
97
- relationName
98
- );
93
+ const targetSource: TableSourceNode = targetTable ?? {
94
+ type: 'Table',
95
+ name: relation.target.name,
96
+ schema: relation.target.schema
97
+ };
98
+ const effectiveTargetName = targetTableName ?? relation.target.name;
99
+ let targetCondition: ExpressionNode = eq(
100
+ { type: 'Column', table: effectiveTargetName, name: targetKey },
101
+ { type: 'Column', table: relation.pivotTable.name, name: relation.pivotForeignKeyToTarget }
102
+ );
103
+
104
+ if (extra) {
105
+ targetCondition = and(targetCondition, extra);
106
+ }
107
+
108
+ const targetJoin = createJoinNode(
109
+ joinKind,
110
+ targetSource,
111
+ targetCondition,
112
+ relationName
113
+ );
99
114
 
100
115
  return [pivotJoin, targetJoin];
101
116
  };
@@ -108,15 +123,16 @@ export const buildBelongsToManyJoins = (
108
123
  * @param rootAlias - Optional alias for the root table
109
124
  * @returns Expression node representing the complete join condition
110
125
  */
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
- };
126
+ export const buildRelationJoinCondition = (
127
+ root: TableDef,
128
+ relation: RelationDef,
129
+ extra?: ExpressionNode,
130
+ rootAlias?: string,
131
+ targetTableName?: string
132
+ ): ExpressionNode => {
133
+ const base = baseRelationCondition(root, relation, rootAlias, targetTableName);
134
+ return extra ? and(base, extra) : base;
135
+ };
120
136
 
121
137
  /**
122
138
  * Builds a relation correlation condition for subqueries
@@ -125,6 +141,11 @@ export const buildRelationJoinCondition = (
125
141
  * @param rootAlias - Optional alias for the root table
126
142
  * @returns Expression node representing the correlation condition
127
143
  */
128
- export const buildRelationCorrelation = (root: TableDef, relation: RelationDef, rootAlias?: string): ExpressionNode => {
129
- return baseRelationCondition(root, relation, rootAlias);
130
- };
144
+ export const buildRelationCorrelation = (
145
+ root: TableDef,
146
+ relation: RelationDef,
147
+ rootAlias?: string,
148
+ targetTableName?: string
149
+ ): ExpressionNode => {
150
+ return baseRelationCondition(root, relation, rootAlias, targetTableName);
151
+ };