Как получить ошибку SQLite в PHP/Laravel, когда цитируемый столбец не существует

Что касается моего предыдущего вопроса, я узнал, что из-за допущенной мной ошибки Laravel генерирует неправильный SQL-запрос:

select * from "companies" where "companies"."id" = '9c54986f-8284-4da9-b826-c7a723de279b' and "companies"."deleted_at" is null and "company_id" = '9c54986f-8284-4da9-b826-c7a723de279b'

Проблема здесь в том, что company_id не существует в companies; однако запрос не генерирует ошибку при запуске, он просто не возвращает результата.

Полагаю, проблема здесь в том, что "company_id" рассматривается как литерал, а не как ссылка на столбец; если я удалю кавычки, я получу правильную ошибку:

Error: in prepare, no such column: company_id (1)

Я также получаю правильную ошибку, если добавляю префикс таблицы к имени столбца:

sqlite> select * from "companies" where "companies"."id" = '9c54986f-8284-4da9-b826-c7a723de279b' and "companies"."deleted_at" is null and "companies"."compa
ny_id" = '9c54986f-8284-4da9-b826-c7a723de279b';
Error: in prepare, no such column: companies.company_id (1)

Есть ли способ решить эту проблему, воздействуя на конфигурацию Laravel или SQLite? Я не могу изменить способ генерации запросов, поскольку они генерируются самой платформой.

Кроме того, я НЕ спрашиваю, почему этот конкретный запрос ведет себя именно так, это мне уже было ясно.

Фрагмент and "company_id" = '9c54986f-8284-4da9-b826-c7a723de279b' генерируется глобальной областью видимости, реализованной следующим образом:

abstract readonly class UnlessAuthorizedScope implements Scope {
    public function __construct(
        private   string  $modelField,
        protected ?string $authorizingPermission,
        private   string  $userField,
    ) {}

    public function apply(Builder $builder, Model $model): void {
        if (Auth::hasUser()) {
            $user = Auth::user();

            if (
                !$this->authorizingPermission
                || !$user?->can($this->authorizingPermission)
            ) {
                $builder->where(
                    $this->modelField,
                    $user?->{$this->userField}
                );
            }
        }
    }
}

который затем реализуется в:

readonly class CurrentCompanyScope extends UnlessAuthorizedScope {
    public function __construct(?string $authorizingPermission = null, ?string $modelField = null) {
        parent::__construct(
            $modelField ?? "company_id",
            $authorizingPermission,
            "company_id"
        );
    }
}

и, наконец, используется как:

class Company extends Model {
    protected static function booted(): void {
        parent::booted();
        static::addGlobalScope(new CurrentCompanyScope(
            CompanyPermission::ViewAll->value,
            // the error was here, instead of specifying "id", I kept the default "company_id" value
        ));
    }
}

🤔 А знаете ли вы, что...
PHP имеет встроенную поддержку сессий для отслеживания состояния пользователей на веб-сайтах.


168
2

Ответы:

Я думаю, что здесь происходит то, что "company_id" интерпретируется как строковый литерал, а не как столбец. Имейте в виду, что SQLite принимает строковые литералы как в одинарных, так и в двойных кавычках. Судя по всему, эвристика SQLite для интерпретации строк в двойных кавычках такова:

  • Сначала проверьте, можно ли сопоставить строку с каким-либо идентификатором базы данных (например, столбцом).
  • Если это не удалось, тогда обрабатывайте строку как строковый литерал.

Вот ваш запрос еще раз в формате:

SELECT *
FROM "companies"
WHERE
    "companies"."id" = '9c54986f-8284-4da9-b826-c7a723de279b' AND
    "companies"."deleted_at" IS NULL AND
    "company_id" = '9c54986f-8284-4da9-b826-c7a723de279b';

Поскольку столбец company_id не существует, этот запрос интерпретируется как:

SELECT *
FROM "companies"
WHERE
    "companies"."id" = '9c54986f-8284-4da9-b826-c7a723de279b' AND
    "companies"."deleted_at" IS NULL AND
    'company_id' = '9c54986f-8284-4da9-b826-c7a723de279b';

Конечно, сравнение строк в последнем члене предложения WHERE никогда не будет истинным, поэтому никакие записи не возвращаются.

Нет необходимости заключать столбцы в двойные кавычки, поэтому не используйте их, чтобы избежать проблем такого рода.


Решено

Мне удалось решить эту проблему, изменив реализацию моей базовой глобальной области:

abstract readonly class UnlessAuthorizedScope implements Scope {
    public function __construct(
        private   string  $modelField,
        protected ?string $authorizingPermission,
        private   string  $userField,
    ) {}

    public function apply(Builder $builder, Model $model): void {
        if (Auth::hasUser()) {
            $user = Auth::user();

            if (
                !$this->authorizingPermission
                || !$user?->can($this->authorizingPermission)
            ) {
                $builder->where(
                    $model->getTable().".".$this->modelField, // changed here
                    $user?->{$this->userField}
                );
            }
        }
    }
}

добавив явный префикс таблицы к имени поля, он сохранялся в запросе, сгенерированном платформой, который затем стал:

select * from "companies" where "companies"."id" = '9c54986f-8284-4da9-b826-c7a723de279b' and "companies"."deleted_at" is null and "companies"."compa
ny_id" = '9c54986f-8284-4da9-b826-c7a723de279b'

и этот новый запрос сгенерировал правильное исключение, которое было перехвачено платформой и отображено на странице.