-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Unknown
-
None
-
Affects Version/s: None
-
None
-
None
-
None
-
None
-
None
-
None
-
None
Problem
Builder::compileWheres() bucketed all "and"-connected wheres and all "or"-connected wheres separately, then merged them as sibling $and and $or keys at the top level. Since MongoDB implicitly ANDs sibling top-level keys, a chain of three clauses or more compiled to the wrong logical expression:
Model::where\('a', 1\)\->where\('b', 2\)\->orWhere\('c', 3\); // compiled to {"$and": \[{a:1},{b:2}\], "$or": \[{c:3}\]} // evaluated by MongoDB as: a AND b AND c
Laravel generates where "a" = ? and "b" = ? or "c" = ? for the same chain, and SQL operator precedence puts AND before OR, so the expected meaning is (a AND b) OR c. Documents matching only the c branch were silently excluded.
The bug was hidden because only the trivial two-clause case was covered by tests, and a special case rewrote the boolean of the first where based on the second one, which happened to produce the right result for two clauses only.
Fix
Group consecutive "and"-connected wheres together and start a new group at each "or", then combine the groups with $or. This mirrors the precedence of the SQL generated by Laravel.
Behavioral change
The generated query changes for any chain of three clauses or more that mixes where and orWhere. compileWheres() is also used by update() and delete(), so an application that relied on the old, more restrictive result will now match a wider set of documents, including on writes:
Post::where\('a', 1\)\->orWhere\('b', 2\)\->where\('owner\_id', $me\->id\)\->delete\(\); // before: \(a OR b\) AND owner \-> the owner filter applied to both branches // after: a OR \(b AND owner\) \-> the a=1 branch is no longer filtered
The new behavior matches Laravel and SQL, so the old one was fail-closed by accident rather than by contract. Applications that want the old grouping should make it explicit with a closure, which is stable across both versions:
\->where\('owner\_id', $me\->id\)\->where\(fn \($q\) => $q\->where\('a', 1\)\->orWhere\('b', 2\)\);
Eloquent global scopes are not affected. callScope() goes through addNewWheresWithinGroup(), so SoftDeletes and multi-tenant scopes stay in their own nested group connected with "and", before and after the change.
Notes
Community contribution: https://github.com/mongodb/laravel-mongodb/pull/3567