Skip to content

fix(query-builder): skip eager relation query when the main query already joins it - #12777

Open
michaelbromley wants to merge 1 commit into
masterfrom
pyongyang
Open

fix(query-builder): skip eager relation query when the main query already joins it#12777
michaelbromley wants to merge 1 commit into
masterfrom
pyongyang

Conversation

@michaelbromley

Copy link
Copy Markdown
Member

Fixes #12775

Under relationLoadStrategy: 'query', every eager relation of the queried entity was fetched by a separate query after the main one, even when the main query had already joined and selected that relation. The relation was therefore read twice, and any column transformer on it ran twice per row.

Since the query strategy no longer joins eager relations into the main query, this is easy to hit: an ORDER BY or a WHERE on a column of an eager relation forces you to create the join yourself, and creating it was enough to cause the second fetch.

The separate load is now skipped when the main query already hydrates the relation exactly as that load would. A join only qualifies when all of the following hold:

  • it is a LEFT join of the relation off the main alias, selected in full, and not mapped onto another property with leftJoinAndMapMany/leftJoinAndMapOne
  • it carries no extra ON condition, and no WHERE or HAVING clause names the joined alias, since either of those hydrates only a subset of the relation
  • the related entity has no eager relations of its own, which a plain join would not load
  • the find options say nothing about the relation's contents through select, order or relations, none of which a plain join reproduces

Anything outside those conditions keeps loading the relation by its own query, so what callers get back is unchanged.

Tests are added to test/functional/relations/load-strategy/query: one asserting a single query against the relation's table when the join covers it, and three asserting the relation is still loaded in full when an ON condition, a WHERE on the joined alias, or the related entity's own eager relations mean the join cannot stand in for it.

@pkg-pr-new

pkg-pr-new Bot commented Aug 11, 2026

Copy link
Copy Markdown

commit: d055783

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Ignores loadEagerRelations flag 🐞 Bug ➹ Performance
Description
isRelationHydratedByJoin() always disqualifies a join when the related entity has eager relations,
even when loadEagerRelations is explicitly false (so the separate load would not include those eager
relations either). This prevents the new skip logic from working in that configuration and can keep
running redundant relation queries under relationLoadStrategy:"query".
Code

src/query-builder/SelectQueryBuilder.ts[R3462-3463]

+        if (relation.inverseEntityMetadata.eagerRelations.length > 0)
+            return false
Evidence
The new method unconditionally rejects joins when the target has eager relations, but the codebase
shows eager loading is skipped entirely when loadEagerRelations === false, and that flag is
propagated into the separate relation-load query path. Therefore, the eager-relations disqualifier
should be conditional on eager loading actually being enabled.

src/query-builder/SelectQueryBuilder.ts[3459-3464]
src/query-builder/SelectQueryBuilder.ts[3727-3789]
src/query-builder/RelationLoader.ts[433-451]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`isRelationHydratedByJoin()` returns `false` whenever `relation.inverseEntityMetadata.eagerRelations.length > 0`, but when `loadEagerRelations === false` the separate relation-load query path will *not* load those eager relations. This makes the new join-hydration equivalence check unnecessarily strict and leaves redundant relation queries in place.

### Issue Context
The separate relation-load query builder propagates `loadEagerRelations` into the child query builder; `RelationLoader` and `applyFindOptions` already honor `loadEagerRelations === false`.

### Fix Focus Areas
- src/query-builder/SelectQueryBuilder.ts[3462-3463]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Soft-delete join blocks skipping 🐞 Bug ➹ Performance
Description
isRelationHydratedByJoin() requires !join.condition, but join() automatically injects a
soft-delete ON predicate (alias.deletedAt IS NULL) for joined entities with a deleteDateColumn
when withDeleted is not enabled. This makes otherwise-unrestricted left joins on soft-deletable
relations never qualify, so the relation can still be loaded twice under
relationLoadStrategy:"query".
Code

src/query-builder/SelectQueryBuilder.ts[R3485-3488]

+                join.entityOrProperty === relationPath &&
+                join.direction === "LEFT" &&
+                !join.condition &&
+                join.mapToProperty === undefined &&
Evidence
The new equivalence check disqualifies any join with join.condition, but the join builder itself
auto-populates joinAttribute.condition with a delete-date predicate for soft-deleted entities.
That means a plain leftJoinAndSelect() can still have a non-empty condition and thus will never be
considered ‘already hydrated’.

src/query-builder/SelectQueryBuilder.ts[3482-3494]
src/query-builder/SelectQueryBuilder.ts[2080-2165]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`isRelationHydratedByJoin()` rejects any join that has a `join.condition`. However, `SelectQueryBuilder.join()` can automatically set `joinAttribute.condition` to the soft-delete predicate (e.g. `${aliasName}.${deleteDateColumn.propertyName} IS NULL`) even when the caller did not supply an ON condition. This blocks the new optimization for soft-deletable related entities, leaving redundant relation-load queries.

### Issue Context
This only applies when the joined entity has a `deleteDateColumn` and the query is *not* `withDeleted`.

### Fix Focus Areas
- src/query-builder/SelectQueryBuilder.ts[3485-3488]
- src/query-builder/SelectQueryBuilder.ts[2131-2139]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

  • Author self-review: I have reviewed the code review findings, and addressed the relevant ones.
Tip of the day
💡 Did you know, you can group findings by type and pick your Finding display, from Minimal to Full

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@sonarqubecloud

Copy link
Copy Markdown

@alumni
alumni requested review from Cprakhar and smith-xyz August 26, 2026 12:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Eager relation is fetched twice when the query already joins and selects it (relationLoadStrategy: 'query')

3 participants