metal-orm 1.0.101 → 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.
- package/dist/index.cjs +57 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +57 -6
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/inflection/pt-br.mjs +468 -445
- package/src/query-builder/relation-include-strategies.ts +35 -4
- package/src/query-builder/relation-join-planner.ts +73 -2
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|