Fix HY093 invalid parameter number when combining search() with an outer join - #234
Open
kima92 wants to merge 1 commit into
Open
Fix HY093 invalid parameter number when combining search() with an outer join#234kima92 wants to merge 1 commit into
kima92 wants to merge 1 commit into
Conversation
…ter join mergeQueries() flattened every binding into the "where" group via array_merge_recursive() + setBindings(), discarding Laravel's binding-type information. getBindings() re-assembles bindings in a fixed group order (select, from, join, where, ...), so a binding belonging to an earlier group on the outer query -- e.g. a bound condition inside a join() closure, whose binding is in the "join" group -- was ordered ahead of the sub-query's own bindings, even though the sub-query's placeholders come first in the compiled SQL. The off-by-one produced "SQLSTATE[HY093]: Invalid parameter number" when a searchable query was combined with such a join and paginated. A top-level where() is unaffected: its binding is in the "where" group, the same group the sub-query bindings are flattened into, so it stays aligned. Register the relevance sub-query with fromRaw() so its bindings stay in the "from" group and the outer query's own bindings remain in their groups, keeping binding order aligned with placeholder order. No change to the generated SQL.
kima92
force-pushed
the
fix/hy093-binding-order-with-outer-join
branch
from
July 11, 2026 14:48
16a69c2 to
d5cbfb7
Compare
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.
Problem
mergeQueries()produces a query with mis-ordered bindings when a searchable query is combined with an outer join and then paginated, throwing:Reproduction
The trigger is a bound condition inside a
join()closure (its binding lands in the query'sjoingroup):The first relevance
LIKE ?ends up bound to$someId, every binding shifts by one, and the parameter count no longer matches the placeholders.Root cause
mergeQueries()wraps the relevance query in a rawFROMsub-query and then flattens every binding into thewheregroup:getBindings()returns a flat array, so this discards Laravel's binding-type information. When the final SQL is assembled,Builder::getBindings()concatenates the buckets in a fixed order —select → from → join → where → having → order → …. Any binding that legitimately belongs to an earlier bucket on the outer query (e.g. a condition inside ajoin()closure, whose binding is in thejoinbucket) is therefore ordered ahead of the sub-query's own bindings — even though, positionally, the sub-query's placeholders appear first in the compiled SQL. The lists no longer line up →HY093.Fix
Register the sub-query with
fromRaw()so its bindings land in thefrombucket (the correct position, beforejoin/where), and leave the outer query's own bindings untouched in their respective buckets:fromRaw()compiles the sub-query identically to the previousfrom(raw(...)); only the binding placement changes.withoutGlobalScopes()behavior is preserved (global scopes are already baked into the sub-query).Testing
Verified against a real Laravel 12 app (MySQL): a searchable model paginated with a
join()closure carrying a boundwherereproducesHY093on the current code and succeeds after this change; the full matrix (search+join, search-only, join-only, neither) passes. A top-levelwhere(in thewheregroup) was confirmed unaffected either way. Generated SQL diffed byte-for-byte identical to the previous implementation.