You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This pull request links relevant issues as Fixes #00000
There are new or updated tests validating the change (tests/**.test.ts)
Documentation has been updated to reflect this change (docs/docs/**.md)
PR Type
Bug fix, Tests
Description
Fix @RelationId fields to respect explicit select statements
Add helper function to check if relationId should be loaded
Apply selection checks to main alias and joined entities
Add comprehensive test suite for relationId select behavior
Diagram Walkthrough
flowchart LR
A["RelationId Transformer"] -->|"Add shouldLoadRelationId helper"| B["Check selection criteria"]
B -->|"No selects defined"| C["Load all relationIds"]
B -->|"Alias selected"| C
B -->|"Property selected"| C
B -->|"Not selected"| D["Skip relationId"]
C -->|"Apply to main alias"| E["Main entity relationIds"]
C -->|"Apply to joins"| F["Joined entity relationIds"]
const shouldLoadRelationId = (
aliasName: string,
relationIdPropertyName: string,
): boolean => {
// If no specific selects are defined, we assume everything is selected (default behavior)
if (this.expressionMap.selects.length === 0) return true
// Check if the whole entity (alias) is selected
- if (selections.has(aliasName)) return true+ if (selections.has(aliasName) || selections.has(aliasName + ".*")) return true
// Check if the specific relationId property is selected
const propertySelection = aliasName + "." + relationIdPropertyName
if (selections.has(propertySelection)) return true
// Check if all columns are selected
if (fullSelectionCache.has(aliasName)) {
return !!fullSelectionCache.get(aliasName)
}
const alias = this.expressionMap.aliases.find(
(alias) => alias.name === aliasName,
)
if (alias && alias.metadata) {
const allColumnsSelected = alias.metadata.columns
.filter((column) => column.isSelect)
.every((column) =>
selections.has(aliasName + "." + column.propertyPath),
)
fullSelectionCache.set(aliasName, allColumnsSelected)
if (allColumnsSelected) return true
} else {
fullSelectionCache.set(aliasName, false)
}
return false
}
Apply / Chat
Suggestion importance[1-10]: 6
__
Why: The suggestion correctly identifies a missing case (alias.*) for whole-entity selection, improving the robustness of the new shouldLoadRelationId logic.
const shouldLoadRelationId = (
aliasName: string,
relationIdPropertyName: string,
): boolean => {
// If no specific selects are defined, we assume everything is selected (default behavior)
if (this.expressionMap.selects.length === 0) return true
// Check if the whole entity (alias) is selected
if (selections.has(aliasName)) return true
++ // Check if all columns are selected via wildcard+ if (selections.has(aliasName + ".*")) return true
// Check if the specific relationId property is selected
const propertySelection = aliasName + "." + relationIdPropertyName
if (selections.has(propertySelection)) return true
// Check if all columns are selected
if (fullSelectionCache.has(aliasName)) {
return !!fullSelectionCache.get(aliasName)
}
const alias = this.expressionMap.aliases.find(
(alias) => alias.name === aliasName,
)
if (alias && alias.metadata) {
const allColumnsSelected = alias.metadata.columns
.filter((column) => column.isSelect)
.every((column) =>
selections.has(aliasName + "." + column.propertyPath),
)
fullSelectionCache.set(aliasName, allColumnsSelected)
if (allColumnsSelected) return true
} else {
fullSelectionCache.set(aliasName, false)
}
return false
}
Suggestion importance[1-10]: 7
__
Why: The suggestion correctly identifies that the new logic does not handle wildcard selections (alias.*), a common pattern that should imply loading all properties, including @RelationId fields.
Medium
Normalize select strings for matching
Normalize selection strings by trimming whitespace before adding them to the Set to prevent matching failures due to formatting differences.
Why: The suggestion improves robustness by trimming whitespace from selection strings, preventing potential mismatches due to formatting variations in user input.
Before adding a new RelationIdAttribute for the main alias, check if an attribute for the same parent alias and relation property already exists in expressionMap.relationIdAttributes to prevent duplicates.
Why: This suggestion correctly points out a potential issue where duplicate RelationIdAttribute instances could be created, leading to redundant SQL and potential errors. Adding a check to prevent duplicates is a critical improvement for robustness.
Medium
De-duplicate joined relation-ids
Before adding a new RelationIdAttribute for a joined alias, check if an attribute for the same parent alias and relation property already exists in expressionMap.relationIdAttributes to prevent duplicates.
Why: This suggestion correctly applies the same logic from the previous suggestion to joined entities, which is necessary for consistency and to prevent the same class of bugs (redundant SQL, conflicts) in queries with joins.
Medium
Handle wildcard column selections
Modify the shouldLoadRelationId function to correctly handle wildcard selections like alias.*, ensuring @RelationId fields are loaded when all columns of an entity are selected via a wildcard.
const shouldLoadRelationId = (
aliasName: string,
relationIdPropertyName: string,
): boolean => {
// If no specific selects are defined, we assume everything is selected (default behavior)
if (this.expressionMap.selects.length === 0) return true
// Check if the whole entity (alias) is selected
if (selections.has(aliasName)) return true
++ // Check wildcard selection (all columns)+ if (selections.has(aliasName + ".*")) return true
// Check if the specific relationId property is selected
const propertySelection = aliasName + "." + relationIdPropertyName
if (selections.has(propertySelection)) return true
// Check if all columns are selected
if (fullSelectionCache.has(aliasName)) {
return !!fullSelectionCache.get(aliasName)
}
const alias = this.expressionMap.aliases.find(
(alias) => alias.name === aliasName,
)
if (alias && alias.metadata) {
const allColumnsSelected = alias.metadata.columns
.filter((column) => column.isSelect)
.every((column) =>
selections.has(aliasName + "." + column.propertyPath),
)
fullSelectionCache.set(aliasName, allColumnsSelected)
if (allColumnsSelected) return true
} else {
fullSelectionCache.set(aliasName, false)
}
return false
}
Suggestion importance[1-10]: 7
__
Why: The suggestion correctly identifies that the new shouldLoadRelationId function does not handle wildcard selections (e.g., alias.*), which would cause @RelationId fields to be omitted incorrectly.
✅ Use select() to test explicit selectionSuggestion Impact:The commit reworked the test setup (removed the initial no-data loadRelationIdAndMap check) and added a new test that uses QueryBuilder.select([...]) with explicit column selection, aiming to validate @RelationId behavior under explicit selections. However, the original test still uses loadRelationIdAndMap and the new select-based test does not select memberIds explicitly as suggested.
code diff:
@@ -63,18 +63,8 @@
Promise.all(
connections.map(async (connection) => {
const groupRepository = connection.getRepository(Group)
+ const userRepository = connection.getRepository(User)- // Use loadRelationIdAndMap to explicitly request the RelationId field- const foundGroups = await groupRepository- .createQueryBuilder("grp")- .select("grp.id")- .loadRelationIdAndMap("grp.memberIds", "grp.members")- .getMany()-- expect(foundGroups.length).to.be.equal(0) // No data seeded in this test, but structure check is key-- // Seeding data for this specific test to verify values- const userRepository = connection.getRepository(User)
const users: User[] = []
for (let i = 0; i < 5; i++) {
users.push(
@@ -85,15 +75,48 @@
group.members = users
await groupRepository.save(group)
- const reloadedGroups = await groupRepository+ const foundGroups = await groupRepository
.createQueryBuilder("grp")
.select("grp.id")
.loadRelationIdAndMap("grp.memberIds", "grp.members")
.getMany()
- expect(reloadedGroups.length).to.be.equal(1)- reloadedGroups.forEach((group) => {+ expect(foundGroups.length).to.be.equal(1)+ foundGroups.forEach((group) => {
expect(group).to.have.property("id")
+ expect(group).to.have.property("memberIds")+ expect(group.memberIds).to.be.an("array")+ expect(group.memberIds).to.have.length(5)+ })+ }),+ ))++ it("should load @RelationId when all columns are explicitly selected", () =>+ Promise.all(+ connections.map(async (connection) => {+ const groupRepository = connection.getRepository(Group)+ const userRepository = connection.getRepository(User)++ const users: User[] = []+ for (let i = 0; i < 5; i++) {+ users.push(+ await userRepository.save({ name: `User ${i + 1}` }),+ )+ }+ const group = await groupRepository.save({ name: "Group 1" })+ group.members = users+ await groupRepository.save(group)++ // Select all columns explicitly+ const foundGroups = await groupRepository+ .createQueryBuilder("grp")+ .select(["grp.id", "grp.name"])+ .getMany()++ expect(foundGroups).to.have.length(1)+ foundGroups.forEach((group) => {+ expect(group).to.have.property("id")+ expect(group).to.have.property("name")
expect(group).to.have.property("memberIds")
expect(group.memberIds).to.be.an("array")
expect(group.memberIds).to.have.length(5)
Modify the test to use select(["grp.id", "grp.memberIds"]) instead of loadRelationIdAndMap to correctly validate the PR's changes to @RelationId handling with explicit selections.
it("should load @RelationId when it IS explicitly selected", () =>
Promise.all(
connections.map(async (connection) => {
const groupRepository = connection.getRepository(Group)
-- // Use loadRelationIdAndMap to explicitly request the RelationId field- const foundGroups = await groupRepository- .createQueryBuilder("grp")- .select("grp.id")- .loadRelationIdAndMap("grp.memberIds", "grp.members")- .getMany()-- expect(foundGroups.length).to.be.equal(0) // No data seeded in this test, but structure check is key+ const userRepository = connection.getRepository(User)
// Seeding data for this specific test to verify values
- const userRepository = connection.getRepository(User)
const users: User[] = []
for (let i = 0; i < 5; i++) {
users.push(
await userRepository.save({ name: `User ${i + 1}` }),
)
}
const group = await groupRepository.save({ name: "Group 1" })
group.members = users
await groupRepository.save(group)
- const reloadedGroups = await groupRepository+ const foundGroups = await groupRepository
.createQueryBuilder("grp")
- .select("grp.id")- .loadRelationIdAndMap("grp.memberIds", "grp.members")+ .select(["grp.id", "grp.memberIds"])
.getMany()
- expect(reloadedGroups.length).to.be.equal(1)- reloadedGroups.forEach((group) => {+ expect(foundGroups.length).to.be.equal(1)+ foundGroups.forEach((group) => {
expect(group).to.have.property("id")
expect(group).to.have.property("memberIds")
+ expect(Object.keys(group).length).to.be.equal(2)
expect(group.memberIds).to.be.an("array")
expect(group.memberIds).to.have.length(5)
})
}),
))
Suggestion importance[1-10]: 9
__
Why: This suggestion correctly identifies a critical flaw in the test logic, where the test uses loadRelationIdAndMap instead of select, failing to validate the actual changes made in the PR.
High
Avoid non-null assertions in callbacks
To improve readability and avoid redundant non-null assertions, assign this.expressionMap.mainAlias to a local variable after the null check and use that variable within the forEach callback.
Why: The suggestion correctly points out redundant non-null assertions and proposes a cleaner, more readable approach by assigning the checked value to a local variable, which is a good practice.
1. No docs for @RelationId change 📘 Rule violation✓ Correctness
Description
The PR changes the selection/loading behavior of @RelationId fields, which is user-facing, but
includes no documentation updates. Users may be surprised by the changed select behavior without
corresponding docs or examples.
+ if (+ !shouldLoadRelationId(+ this.expressionMap.mainAlias!.name,+ relationId.propertyName,+ )+ )+ return+
Evidence
Compliance requires documentation updates for user-facing behavior changes. The diff adds logic that
conditionally skips loading relation-id attributes based on selects, but no docs changes are
present in the PR diff.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
The PR changes `@RelationId` selection/loading behavior but does not update documentation for this user-facing behavior.
## Issue Context
New logic conditionally skips creating/loading relation-id attributes depending on query `selects`, which can change what properties appear on returned entities.
## Fix Focus Areas
- src/query-builder/relation-id/RelationIdMetadataToAttributeTransformer.ts[57-64]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
The new functional test performs an initial query expecting 0 results and includes explanatory
commentary that reads as non-essential noise. This adds AI-like verbosity and unnecessary steps that
can be removed without reducing test coverage.
+ // Use loadRelationIdAndMap to explicitly request the RelationId field+ const foundGroups = await groupRepository+ .createQueryBuilder("grp")+ .select("grp.id")+ .loadRelationIdAndMap("grp.memberIds", "grp.members")+ .getMany()++ expect(foundGroups.length).to.be.equal(0) // No data seeded in this test, but structure check is key++ // Seeding data for this specific test to verify values
Evidence
The compliance rule requires avoiding AI-generated slop such as extra commentary and
abnormal/unnecessary code. The added test first asserts an empty result set before seeding data,
accompanied by a comment justifying it; this is redundant for the stated goal and can be simplified.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
The test includes an unnecessary pre-seed query expecting zero results and a verbose explanatory comment, which adds noise without improving coverage.
## Issue Context
The test can validate `@RelationId` explicit selection by seeding data first and then running a single query/assertion pass.
## Fix Focus Areas
- test/functional/query-builder/relation-id/select-behavior/relation-id-select-behavior.test.ts[67-76]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
@RelationId loading is now gated on selecting the whole alias (e.g. "grp"), but entity hydration
also supports fully selecting an entity via selecting all its columns individually. This can lead to
fully-hydrated entities unexpectedly missing @RelationId fields depending on how the select list is
written.
+ // Helper function to check if a relationId should be loaded+ const shouldLoadRelationId = (+ aliasName: string,+ relationIdPropertyName: string,+ ): boolean => {+ // If no specific selects are defined, we assume everything is selected (default behavior)+ if (this.expressionMap.selects.length === 0) return true++ // Check if the whole entity (alias) is selected+ if (+ this.expressionMap.selects.some(+ (s) => s.selection === aliasName,+ )+ )+ return true++ // Check if the specific relationId property is selected+ const propertySelection = aliasName + "." + relationIdPropertyName+ if (+ this.expressionMap.selects.some(+ (s) => s.selection === propertySelection,+ )+ )+ return true+
Evidence
The new gating only returns true when the alias itself is selected (or when an
alias.relationIdPropertyName string is selected), otherwise it skips adding the RelationIdAttribute.
But entity transformation treats either selecting the alias OR selecting individual alias.column
paths as sufficient to include column values, so callers can fully select an entity without ever
selecting the alias token—leading to a mismatch where the entity is fully populated but @RelationId
is skipped.
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
`shouldLoadRelationId` only considers the alias token (e.g. `grp`) as indicating the entity is selected. However, TypeORM can hydrate a full entity when all of its columns are individually selected (e.g. `select([&amp;amp;quot;grp.id&amp;amp;quot;, &amp;amp;quot;grp.name&amp;amp;quot;])`). With the new gating, these two equivalent ways of fully selecting an entity can yield different outcomes for decorator-based `@RelationId` fields.
### Issue Context
- Entity hydration includes columns when either `alias` OR `alias.column` appears in `expressionMap.selects`.
- The new gating only checks `alias` (and an `alias.relationIdPropertyName` string), so `@RelationId` can be skipped even when the entity is fully populated via explicit column list.
### Fix Focus Areas
- src/query-builder/relation-id/RelationIdMetadataToAttributeTransformer.ts[16-95]
- test/functional/query-builder/relation-id/select-behavior/relation-id-select-behavior.test.ts[1-103]
### Implementation notes
- Precompute `const selections = new Set(this.expressionMap.selects.map(s =&amp;amp;gt; s.selection))`.
- For each alias+metadata, compute `isFullySelected`:
- `true` if `selections.has(aliasName)`
- OR if **all** `metadata.columns.filter(c =&amp;amp;gt; c.isSelect)` satisfy `selections.has(`${aliasName}.${c.propertyPath}`)`
- Use `isFullySelected` in `shouldLoadRelationId` (or inline logic) to decide whether to load decorator-based `@RelationId`.
- Add a test that selects all Group columns individually (e.g. `grp.id` and `grp.name`) and asserts `memberIds` is loaded (or, if desired behavior is the opposite, add an explicit test asserting it is not loaded and update the gating/comment accordingly).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
The new helper repeatedly linearly scans expressionMap.selects for every relationId across main and
join aliases. This is low impact but avoidable by precomputing a Set of selections once per
transform().
+ // Helper function to check if a relationId should be loaded+ const shouldLoadRelationId = (+ aliasName: string,+ relationIdPropertyName: string,+ ): boolean => {+ // If no specific selects are defined, we assume everything is selected (default behavior)+ if (this.expressionMap.selects.length === 0) return true++ // Check if the whole entity (alias) is selected+ if (+ this.expressionMap.selects.some(+ (s) => s.selection === aliasName,+ )+ )+ return true++ // Check if the specific relationId property is selected+ const propertySelection = aliasName + "." + relationIdPropertyName+ if (+ this.expressionMap.selects.some(+ (s) => s.selection === propertySelection,+ )+ )+ return true++ return false+ }
Evidence
transform() iterates all relationId metadata entries for the main alias and each join alias, calling
shouldLoadRelationId each time. shouldLoadRelationId performs up to two .some() scans over
expressionMap.selects per call, multiplying work by (#relationIds * #selects).
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution
## Issue description
`shouldLoadRelationId` performs multiple linear scans over `expressionMap.selects` for each relationId on each alias. While likely small in practice, it’s easy to eliminate and simplifies the logic.
### Issue Context
The transformer loops over all `relationIds` for the main alias and joined aliases, calling `shouldLoadRelationId` for each.
### Fix Focus Areas
- src/query-builder/relation-id/RelationIdMetadataToAttributeTransformer.ts[16-95]
### Implementation notes
- Compute once: `const selections = new Set(this.expressionMap.selects.map(s =&amp;amp;gt; s.selection))`.
- Replace `.some(...)` checks with `selections.has(...)`.
- If you implement the ‘effective full selection’ fix from the other finding, you can also cache `isFullySelectedByAliasName` in a `Map&amp;amp;lt;string, boolean&amp;amp;gt;` to avoid recomputing per relationId.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
ⓘ The new review experience is currently in Beta. Learn more
coverage: 81.44% (+0.02%) from 81.419%
when pulling 420569d on gioboa:fix/11483
into f47246c on typeorm:master.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Close #11483
Description of change
Pull-Request Checklist
masterbranchFixes #00000tests/**.test.ts)docs/docs/**.md)