Skip to content

SQL Binding Error with BelongsTo Field and Custom defaultOrderings (coming from Relatable rule) #6875

Closed
@aostapchenko-emp

Description

@aostapchenko-emp

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:

  1. Create a Nova resources: City and State;
  2. Override defaultOrderings in State 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.

  1. Reference this resource via a BelongsTo field in another Nova resource, for example: City
            Fields\BelongsTo::make('State', resource: State::class),
  1. In a form (e.g., Create or Update), submit a value for this BelongsTo field (e.g. select the state);
  2. 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();
// ...

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugVerified bug by the Nova teamfix incomingA fix is in review

    Type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions