fix(model): emit flat joins for belongsTo-chain nested includes (#3245)#3249
Draft
bpamiri wants to merge 1 commit into
Draft
fix(model): emit flat joins for belongsTo-chain nested includes (#3245)#3249bpamiri wants to merge 1 commit into
bpamiri wants to merge 1 commit into
Conversation
The $fromClause join builder decided whether to wrap inner joins in a parenthesized group bound to the LEFT OUTER JOIN purely from a regex on the include string (any `Intermediate(Target)` shape). That issue #449 HABTM/through grouping over-fired on plain belongsTo-chain nested includes such as `include="SecondaryContact(User)"`, nesting an inner join whose ON clause references the root FROM table — which scopes the root out and makes MySQL reject it with "Unknown column ... in 'on clause'". This was a regression from Wheels 2's flat joins. Consult the association metadata instead: only set needsNesting when the parenthesized intermediate association is a genuine hasMany/hasOne bridge (the OUTER-joined case the grouping was designed for). A belongsTo intermediate now falls through to the flat-join branch, restoring Wheels 2 behavior, while real HABTM/through includes still nest unchanged. Verified on SQLite and MySQL 9.7: crudSpec 163/0/0 (incl. new #3245 + #449 regression specs) and hasManyShortcutSpec 13/0/0; full model suite 923/0/0 on SQLite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Peter Amiri <petera@pai.com>
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Closes #3245
Problem
findAll(include="ParentModel(ChildModel)")regressed between Wheels 2 and 3: when the parenthesized intermediate is a plainbelongsToand the include mixes inner + outer joins,$fromClausewrapped the inner joins in a parenthesized group bound to theLEFT OUTER JOIN. That nested join expression scopes the rootFROMtable out of the inner join'sONclause, so MySQL rejects it with:The reporter's case:
Relationship belongsTo SecondaryContact belongsTo User, called asfindAll(include="RelationshipType,SecondaryContact(User)").Root cause
vendor/wheels/model/sql.cfc$fromClausederivedhasThroughAssociationpurely from a regex on the include string (^([^(]+)\(([^)]+)\)$). AnyIntermediate(Target)shape matched — including abelongsTochain — so the issue #449 HABTM/throughparenthesized-grouping heuristic over-fired. #449 introduced that grouping (commit6579f246c, July 2025), which is why this is absent in Wheels 2.Fix
Consult the association metadata instead of the string. When the single-paren pattern matches, look up the parenthesized intermediate association (
ListLastof the text before() invariables.wheels.class.associationsand only setneedsNestingwhen itstypeishasMany/hasOne— the OUTER-joined bridge the grouping was actually designed for. AbelongsTointermediate falls through to the existing flat-join branch (the Wheels 2 behavior).Before (bug) vs after, for
model("author").$fromClause(include="posts,user(galleries)"):The genuine HABTM/
throughcase (team→memberTeams(member), ahasManybridge) still nests:Tests
Two regression specs added to the directly-callable
$fromClauseharness invendor/wheels/tests/specs/model/crudSpec.cfc:throughbridge include asserts the parenthesized grouping is preserved (JOINs should be grouped #449 does not regress).Test status
Verified locally on SQLite (Lucee 7) and MySQL 9.7 (Lucee 7) — the engine in the report:
crudSpec163 pass / 0 fail / 0 error on both engines (includes both new regression specs).hasManyShortcutSpec(live HABTM/through/shortcut eager-load DB queries — JOINs should be grouped #449/model: hasMany shortcut= breaks the association entirely — $expandThroughAssociations misreads the shortcut through-chain as an include path (documented many-to-many pattern throws AssociationNotFound) #3109/hasMany shortcut association not recognised by include - "association not found" error #3208) 13/0/0 on both.wheels.tests.specs.modeldirectory 923/0/0 on SQLite.Cross-engine notes
Pure metadata read using
StructKeyExists/ListFindNoCase/ListLast/Mid— no closures, reserved scopes,attributeCollection=arguments,obj.map(), or barecfabort. The.typefield is set on every association (belongsTo/hasMany/hasOne). Behavior is adapter-independent (the change only decides flat vs nested join structure); MySQL was the engine in the report and is verified green.🤖 Generated with Claude Code