Description
- Laravel Version: 11.45.1 | 12.18.0
- Nova Version: 4.35.10 | 5.7.2
- PHP Version: (not relevant)
- Database Driver & Version: PgSQL without Scout | SQLite without Scout
- Operating System and Version: (not relevant)
- Browser type and version: (not relevant)
- Reproduction Repository: https://github.com/aostapchenko-emp/laravel-nova-bug-relatable-query-order-bindings
Description:
When using a BelongsTo
field in a Nova resource that references another resource with a custom defaultOrderings()
implementation (e.g., ordering based on query-bound parameters), creating or updating records throws a query exception. This occurs because the Relatable
rule used during validation clears the query's orders array but not the associated order bindings and resulting in a binding mismatch.
Detailed steps to reproduce the issue on a fresh Nova installation:
- Create a Nova resources:
City
andState
; - Override
defaultOrderings
inState
resource:
#[Override]
public static function defaultOrderings($query): Builder|Relation
{
return $query->orderByRaw("country_id = ? desc", Country::USA_ID)
->orderBy('country_id')
->orderBy('name');
}
This leads to a query that have states list ordered so USA states go first and then all other countries states:
select * from "state" order by country_id = ? desc, "country_id" asc, "name" asc
and 1 bound parameter passed to that query.
- Reference this resource via a
BelongsTo
field in another Nova resource, for example:City
Fields\BelongsTo::make('State', resource: State::class),
- In a form (e.g., Create or Update), submit a value for this
BelongsTo
field (e.g. select the state); - This causes a
Illuminate\Database\QueryException
:
SQLSTATE[HY000]: General error: 25 column index out of range (Connection: sqlite, SQL: select * from "states" where "states"."id" = 2 limit 1)
Findings and possible way to fix
The issue originates from the internal workings of the Laravel\Nova\Rules\Relatable
validation rule, which is applied to BelongsTo fields during form submission. When this rule runs, it uses the resource search query and explicitly clears the orders clause:
// src/Rules/Relatable.php
// ...
public function passes($attribute, $value)
{
$model = $this->query->tap(function ($query) {
tap($query->getQuery(), function ($builder) {
/** @var \Illuminate\Database\Query\Builder $builder */
$builder->orders = []; // <<<-------------------------------------------------------- HERE
$builder->select(
! empty($builder->joins) ? $builder->from.'.*' : '*'
);
});
})->whereKey($value)->first();
// ...
However, it does not clear the associated order bindings from the query:
dd($builder->bindings['order']); // remains untouched
This leads to a mismatch between the number of placeholders (?) in the raw order by SQL and the actual bindings applied during validation. Since those bindings are left in place even after the ordering logic is removed, the final SQL throws a binding error at runtime.
To prevent that I see the next line can be added next to orders clearing:
// src/Rules/Relatable.php
// ...
public function passes($attribute, $value)
{
$model = $this->query->tap(function ($query) {
tap($query->getQuery(), function ($builder) {
/** @var \Illuminate\Database\Query\Builder $builder */
$builder->orders = [];
$builder->setBindings([], 'order'); // <<<-------------------------------------------------------- HERE
$builder->select(
! empty($builder->joins) ? $builder->from.'.*' : '*'
);
});
})->whereKey($value)->first();
// ...