-
Notifications
You must be signed in to change notification settings - Fork 11.8k
Description
Description
When eager loading a latestOfMany/ofMany relationship with constraints, the constraints are only applied to the outer query and not to the inner subquery that determines which record is the "latest." This causes silently incorrect results — no error is thrown, but the wrong record is returned.
Steps To Reproduce
Given a User model with a lastPayment relationship:
public function payments(): HasMany
{
return $this->hasMany(Payment::class, 'payer_id');
}
public function lastPayment(): HasOne
{
return $this->payments()->one()->latestOfMany();
}When eager loading with a constraint:
User::with(['lastPayment' => fn ($query) => $query
->where('store_id', $storeId)
->where('type', 'store_payment')
])->get();Expected behavior
The constraints should be applied inside the inner subquery so "latest" is determined within the scoped dataset:
SELECT payments.* FROM payments
INNER JOIN (
SELECT MAX(payments.id) as id_aggregate, payments.payer_id
FROM payments
WHERE payments.payer_id IN (...)
AND store_id = ? -- ✅ applied here
AND type = 'store_payment' -- ✅ applied here
GROUP BY payments.payer_id
) AS latestOfMany ON ...
WHERE payments.deleted_at IS NULL;Actual behavior
Constraints only on the outer query. The inner subquery picks the latest across all records, then the outer filter discards it:
SELECT payments.* FROM payments
INNER JOIN (
SELECT MAX(payments.id) as id_aggregate, payments.payer_id
FROM payments
WHERE payments.payer_id IN (...)
-- ❌ no constraints here
GROUP BY payments.payer_id
) AS latestOfMany ON ...
WHERE store_id = ? -- only here
AND type = 'store_payment' -- only hereThis silently returns null for users whose latest payment overall was at a different store — even if they have payments at the target store.
Why This Matters
- Silent data loss: No error, the relationship just returns
nullor the wrong record - Unintuitive:
with(['relation' => fn($q) => $q->where(...)])scopes the entire operation for every other relationship type - Historical context: PR [8.x] Improve one-of-many performance #37451 (Laravel 8.x) specifically moved constraints into the subquery for correctness and performance (212x speedup). Current behavior appears to be a regression from that intent
Workaround
Use ofMany closure to bake constraints at definition time:
return $this->payments()->one()->ofMany(
['id' => 'max'],
fn ($query) => $query->where('store_id', $storeId)->where('type', 'store_payment'),
);Works but requires a separate relationship per constraint set — no dynamic constraints at eager load time.
Versions
- Laravel: 12.55.1
- PHP: 8.5