metal-orm 1.0.100 → 1.0.102

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.
@@ -16,6 +16,7 @@ import { RelationIncludeOptions } from './relation-types.js';
16
16
  import { makeRelationAlias } from './relation-alias.js';
17
17
  import { buildDefaultPivotColumns } from './relation-utils.js';
18
18
  import { findPrimaryKey } from './hydration-planner.js';
19
+ import { getJoinRelationName } from '../core/ast/join-metadata.js';
19
20
 
20
21
  type RelationWithForeignKey =
21
22
  | HasManyRelation
@@ -39,18 +40,42 @@ type IncludeStrategyContext = {
39
40
 
40
41
  type IncludeStrategy = (context: IncludeStrategyContext) => RelationResult;
41
42
 
43
+ /**
44
+ * Gets the correlation name (exposed name) for a join associated with a relation.
45
+ * This is necessary to correctly reference columns when a table has been aliased
46
+ * to avoid "same exposed names" errors in SQL Server.
47
+ */
48
+ const getJoinCorrelationName = (
49
+ state: SelectQueryState,
50
+ relationName: string,
51
+ fallback: string
52
+ ): string => {
53
+ const join = state.ast.joins.find(j => getJoinRelationName(j) === relationName);
54
+ if (!join) return fallback;
55
+
56
+ const t = join.table;
57
+ if (t.type === 'Table') return t.alias ?? t.name;
58
+ if (t.type === 'DerivedTable') return t.alias;
59
+ if (t.type === 'FunctionTable') return t.alias ?? fallback;
60
+ return fallback;
61
+ };
62
+
42
63
  const buildTypedSelection = (
43
64
  columns: Record<string, ColumnDef>,
44
65
  prefix: string,
45
66
  keys: string[],
46
- missingMsg: (col: string) => string
67
+ missingMsg: (col: string) => string,
68
+ tableOverride?: string
47
69
  ): Record<string, ColumnDef> => {
48
70
  return keys.reduce((acc, key) => {
49
71
  const def = columns[key];
50
72
  if (!def) {
51
73
  throw new Error(missingMsg(key));
52
74
  }
53
- acc[makeRelationAlias(prefix, key)] = def;
75
+ // Clone the column definition with the overridden table if provided
76
+ acc[makeRelationAlias(prefix, key)] = tableOverride
77
+ ? { ...def, table: tableOverride }
78
+ : def;
54
79
  return acc;
55
80
  }, {} as Record<string, ColumnDef>);
56
81
  };
@@ -100,11 +125,14 @@ const standardIncludeStrategy: IncludeStrategy = context => {
100
125
  hydration = fkSelectionResult.hydration;
101
126
 
102
127
  const targetColumns = resolveTargetColumns(relation, context.options);
128
+ // Get the actual correlation name from the JOIN (may be aliased to avoid collisions)
129
+ const tableOverride = getJoinCorrelationName(state, context.relationName, relation.target.name);
103
130
  const targetSelection = buildTypedSelection(
104
131
  relation.target.columns as Record<string, ColumnDef>,
105
132
  context.aliasPrefix,
106
133
  targetColumns,
107
- key => `Column '${key}' not found on relation '${context.relationName}'`
134
+ key => `Column '${key}' not found on relation '${context.relationName}'`,
135
+ tableOverride
108
136
  );
109
137
 
110
138
  const relationSelectionResult = context.selectColumns(state, hydration, targetSelection);
@@ -127,11 +155,14 @@ const belongsToManyStrategy: IncludeStrategy = context => {
127
155
  let { state, hydration } = context;
128
156
 
129
157
  const targetColumns = resolveTargetColumns(relation, context.options);
158
+ // Get the actual correlation name from the JOIN (may be aliased to avoid collisions)
159
+ const tableOverride = getJoinCorrelationName(state, context.relationName, relation.target.name);
130
160
  const targetSelection = buildTypedSelection(
131
161
  relation.target.columns as Record<string, ColumnDef>,
132
162
  context.aliasPrefix,
133
163
  targetColumns,
134
- key => `Column '${key}' not found on relation '${context.relationName}'`
164
+ key => `Column '${key}' not found on relation '${context.relationName}'`,
165
+ tableOverride
135
166
  );
136
167
 
137
168
  const pivotAliasPrefix = context.options?.pivot?.aliasPrefix ?? `${context.aliasPrefix}_pivot`;
@@ -8,6 +8,67 @@ import { JoinKind } from '../core/sql/sql.js';
8
8
  import { buildRelationJoinCondition, buildBelongsToManyJoins } from './relation-conditions.js';
9
9
  import { createJoinNode } from '../core/ast/join-node.js';
10
10
 
11
+ /**
12
+ * Gets the exposed name from a TableSourceNode.
13
+ * The exposed name is the alias if present, otherwise the table name.
14
+ */
15
+ const getExposedName = (ts: TableSourceNode): string | null => {
16
+ if (ts.type === 'Table') return ts.alias ?? ts.name;
17
+ if (ts.type === 'DerivedTable') return ts.alias;
18
+ if (ts.type === 'FunctionTable') return ts.alias ?? ts.name;
19
+ return null;
20
+ };
21
+
22
+ /**
23
+ * Collects all exposed names from the current query state (FROM + JOINs).
24
+ * This is used to detect naming collisions when adding new joins.
25
+ */
26
+ const collectExposedNames = (state: SelectQueryState): Set<string> => {
27
+ const used = new Set<string>();
28
+ const fromName = getExposedName(state.ast.from);
29
+ if (fromName) used.add(fromName);
30
+
31
+ for (const j of state.ast.joins) {
32
+ const n = getExposedName(j.table);
33
+ if (n) used.add(n);
34
+ }
35
+ return used;
36
+ };
37
+
38
+ /**
39
+ * Creates a unique alias based on a base name, avoiding collisions with already-used names.
40
+ */
41
+ const makeUniqueAlias = (base: string, used: Set<string>): string => {
42
+ let alias = base;
43
+ let i = 2;
44
+ while (used.has(alias)) alias = `${base}_${i++}`;
45
+ return alias;
46
+ };
47
+
48
+ /**
49
+ * Ensures a TableSourceNode has a unique correlation name (alias) to avoid SQL Server's
50
+ * "same exposed names" error. If the table's exposed name already exists in the query,
51
+ * an alias is generated using the relation name.
52
+ */
53
+ const ensureCorrelationName = (
54
+ state: SelectQueryState,
55
+ relationName: string,
56
+ ts: TableSourceNode,
57
+ extraUsed?: Iterable<string>
58
+ ): TableSourceNode => {
59
+ if (ts.type !== 'Table') return ts;
60
+ if (ts.alias) return ts;
61
+
62
+ const used = collectExposedNames(state);
63
+ for (const x of extraUsed ?? []) used.add(x);
64
+
65
+ // Only alias if the exposed name (table name) already exists
66
+ if (!used.has(ts.name)) return ts;
67
+
68
+ const alias = makeUniqueAlias(relationName, used);
69
+ return { ...ts, alias };
70
+ };
71
+
11
72
  export class RelationJoinPlanner {
12
73
  constructor(
13
74
  private readonly table: TableDef,
@@ -24,11 +85,19 @@ export class RelationJoinPlanner {
24
85
  ): SelectQueryState {
25
86
  const rootAlias = state.ast.from.type === 'Table' ? state.ast.from.alias : undefined;
26
87
  if (relation.type === RelationKinds.BelongsToMany) {
27
- const targetTableSource: TableSourceNode = tableSource ?? {
88
+ let targetTableSource: TableSourceNode = tableSource ?? {
28
89
  type: 'Table',
29
90
  name: relation.target.name,
30
91
  schema: relation.target.schema
31
92
  };
93
+ // Ensure unique alias to avoid "same exposed names" error
94
+ // Include pivot table name in extraUsed since it will be added in this operation
95
+ targetTableSource = ensureCorrelationName(
96
+ state,
97
+ relationName,
98
+ targetTableSource,
99
+ [relation.pivotTable.name]
100
+ );
32
101
  const targetName = this.resolveTargetTableName(targetTableSource, relation);
33
102
  const joins = buildBelongsToManyJoins(
34
103
  this.table,
@@ -43,11 +112,13 @@ export class RelationJoinPlanner {
43
112
  return joins.reduce((current, join) => this.astService(current).withJoin(join), state);
44
113
  }
45
114
 
46
- const targetTable: TableSourceNode = tableSource ?? {
115
+ let targetTable: TableSourceNode = tableSource ?? {
47
116
  type: 'Table',
48
117
  name: relation.target.name,
49
118
  schema: relation.target.schema
50
119
  };
120
+ // Ensure unique alias to avoid "same exposed names" error
121
+ targetTable = ensureCorrelationName(state, relationName, targetTable);
51
122
  const targetName = this.resolveTargetTableName(targetTable, relation);
52
123
  const condition = buildRelationJoinCondition(
53
124
  this.table,