Pairity/src/Database/Query/Builder.php

1281 lines
32 KiB
PHP
Raw Normal View History

2026-02-08 05:26:07 +00:00
<?php
declare(strict_types=1);
namespace Pairity\Database\Query;
use Pairity\Contracts\Database\ConnectionInterface;
use Pairity\Exceptions\DatabaseException;
/**
* Class Builder
*
* Fluent query builder for constructing SQL queries.
*/
class Builder
{
/**
* @var string|Builder|null
*/
public $from = null;
/**
* @var array|null
*/
public ?array $columns = null;
/**
* @var bool
*/
public bool $distinct = false;
/**
* @var array
*/
public array $joins = [];
/**
* @var array
*/
public array $wheres = [];
/**
* @var array
*/
public array $groups = [];
/**
* @var array
*/
public array $havings = [];
/**
* @var array
*/
public array $orders = [];
/**
* @var int|null
*/
public ?int $limit = null;
/**
* @var int|null
*/
public ?int $offset = null;
/**
* @var bool|string|null
*/
public bool|string|null $lock = null;
/**
* @var array|null
*/
public ?array $aggregate = null;
/**
* @var string|null
*/
protected ?string $dtoClass = null;
/**
* @var object|null
*/
protected ?object $dao = null;
/**
* @var array
*/
protected array $eagerLoad = [];
/**
* @var array
*/
protected array $bindings = [
'update' => [],
'select' => [],
'join' => [],
'where' => [],
'having' => [],
'order' => [],
'union' => [],
];
/**
* @var array
*/
public array $unions = [];
/**
* @var int|null
*/
protected ?int $cacheSeconds = null;
/**
* @var bool
*/
protected bool $withoutTenancy = false;
/**
* Builder constructor.
*
* @param \Pairity\Contracts\Database\DatabaseManagerInterface $db
* @param ConnectionInterface $connection
* @param Grammar $grammar
*/
public function __construct(
protected \Pairity\Contracts\Database\DatabaseManagerInterface $db,
protected ConnectionInterface $connection,
protected Grammar $grammar
) {
}
/**
* Set the relationships that should be eager loaded.
*
* @param array|string $relations
* @param string|\Closure|null $callback
* @return $this
*/
public function with(array|string $relations, string|\Closure|null $callback = null): self
{
if (is_string($relations)) {
if ($callback instanceof \Closure) {
$this->eagerLoad[$relations] = $callback;
} elseif (is_string($callback)) {
$this->eagerLoad[$relations] = function ($query) use ($callback) {
$query->select($callback);
};
} else {
$this->eagerLoad[$relations] = function () {};
}
} else {
foreach ($relations as $name => $constraints) {
if (is_numeric($name)) {
$this->with($constraints);
} else {
$this->with($name, $constraints);
}
}
}
return $this;
}
/**
* Get the relationships that should be eager loaded.
*
* @return array
*/
public function getEagerLoads(): array
{
return $this->eagerLoad;
}
/**
* @return \Pairity\Contracts\Database\DatabaseManagerInterface
*/
public function getDb(): \Pairity\Contracts\Database\DatabaseManagerInterface
{
return $this->db;
}
/**
* @return ConnectionInterface
*/
public function getConnection(): ConnectionInterface
{
return $this->connection;
}
/**
* Get the query grammar instance.
*
* @return Grammar
*/
public function getGrammar(): Grammar
{
return $this->grammar;
}
/**
* Set the columns to be selected.
*
* @param array|string|Expression $columns
* @return $this
*/
public function select(array|string|Expression $columns = ['*']): self
{
$columns = is_array($columns) ? $columns : func_get_args();
if ($columns === ['*']) {
$this->columns = $columns;
return $this;
}
foreach ($columns as $as => $column) {
if ($column instanceof \Closure || $column instanceof Builder) {
if (is_string($as)) {
$this->selectSubquery($column, $as);
} else {
throw new DatabaseException('Select subqueries must be aliased.');
}
} elseif (is_string($as) && ($column instanceof Expression || is_string($column))) {
$this->columns[] = [$column, $as];
} else {
$this->columns[] = $column;
}
}
return $this;
}
/**
* Add a subquery to the select clause.
*
* @param \Closure|Builder $query
* @param string $as
* @return $this
*/
protected function selectSubquery(\Closure|Builder $query, string $as): self
{
if ($query instanceof \Closure) {
$callback = $query;
$query = $this->newQuery();
$callback($query);
}
$this->columns[] = [$query, $as];
return $this;
}
/**
* Force the query to only return distinct results.
*
* @return $this
*/
public function distinct(): self
{
$this->distinct = true;
return $this;
}
/**
* Set the table which the query is targeting.
*
* @param string|\Closure|Builder $table
* @param string|null $as
* @return $this
*/
public function from(string|\Closure|Builder $table, ?string $as = null): self
{
if ($table instanceof \Closure) {
return $this->fromSubquery($table, $as);
}
if ($table instanceof Builder) {
return $this->fromSubquery($table, $as);
}
$this->from = $as ? "{$table} as {$as}" : $table;
return $this;
}
/**
* Set the table which the query is targeting as a subquery.
*
* @param \Closure|Builder $query
* @param string|null $as
* @return $this
*/
protected function fromSubquery(\Closure|Builder $query, ?string $as = null): self
{
if ($query instanceof \Closure) {
$callback = $query;
$query = $this->newQuery();
$callback($query);
}
$this->from = [$query, $as ?: 'sub'];
return $this;
}
/**
* Create a new query builder instance.
*
* @return Builder
*/
public function newQuery(): Builder
{
return new Builder($this->db, $this->connection, $this->grammar);
}
/**
* Add a basic where clause to the query.
*
* @param string $column
* @param mixed $operator
* @param mixed $value
* @param string $boolean
* @return $this
*/
public function where(string $column, mixed $operator = null, mixed $value = null, string $boolean = 'and'): self
{
if (func_num_args() === 2) {
$value = $operator;
$operator = '=';
}
$type = 'Basic';
$this->wheres[] = compact('type', 'column', 'operator', 'value', 'boolean');
return $this;
}
/**
* Add an "or where" clause to the query.
*
* @param string $column
* @param mixed $operator
* @param mixed $value
* @return $this
*/
public function orWhere(string $column, mixed $operator = null, mixed $value = null): self
{
if (func_num_args() === 2) {
$value = $operator;
$operator = '=';
}
return $this->where($column, $operator, $value, 'or');
}
/**
* Add a "where null" clause to the query.
*
* @param string $column
* @param string $boolean
* @param bool $not
* @return $this
*/
public function whereNull(string $column, string $boolean = 'and', bool $not = false): self
{
$type = $not ? 'NotNull' : 'Null';
$this->wheres[] = compact('type', 'column', 'boolean');
return $this;
}
/**
* Add an "or where null" clause to the query.
*
* @param string $column
* @return $this
*/
public function orWhereNull(string $column): self
{
return $this->whereNull($column, 'or');
}
/**
* Add a "where not null" clause to the query.
*
* @param string $column
* @param string $boolean
* @return $this
*/
public function whereNotNull(string $column, string $boolean = 'and'): self
{
return $this->whereNull($column, $boolean, true);
}
/**
* Add an "or where not null" clause to the query.
*
* @param string $column
* @return $this
*/
public function orWhereNotNull(string $column): self
{
return $this->whereNotNull($column, 'or');
}
/**
* Add a "where exists" clause to the query.
*
* @param \Closure|Builder $callback
* @param string $boolean
* @param bool $not
* @return $this
*/
public function whereExists(\Closure|Builder $callback, string $boolean = 'and', bool $not = false): self
{
$type = $not ? 'NotExists' : 'Exists';
if ($callback instanceof \Closure) {
$query = $this->newQuery();
$callback($query);
} else {
$query = $callback;
}
$this->wheres[] = compact('type', 'query', 'boolean');
return $this;
}
/**
* Add an "or where exists" clause to the query.
*
* @param \Closure|Builder $callback
* @return $this
*/
public function orWhereExists(\Closure|Builder $callback): self
{
return $this->whereExists($callback, 'or');
}
/**
* Add a "where not exists" clause to the query.
*
* @param \Closure|Builder $callback
* @param string $boolean
* @return $this
*/
public function whereNotExists(\Closure|Builder $callback, string $boolean = 'and'): self
{
return $this->whereExists($callback, $boolean, true);
}
/**
* Add an "or where not exists" clause to the query.
*
* @param \Closure|Builder $callback
* @return $this
*/
public function orWhereNotExists(\Closure|Builder $callback): self
{
return $this->whereNotExists($callback, 'or');
}
/**
* Add a "where in" clause to the query.
*
* @param string $column
* @param mixed $values
* @param string $boolean
* @param bool $not
* @return $this
*/
public function whereIn(string $column, mixed $values, string $boolean = 'and', bool $not = false): self
{
$type = $not ? 'NotIn' : 'In';
if ($values instanceof \Closure || $values instanceof Builder) {
return $this->whereInSubquery($column, $values, $boolean, $not);
}
$this->wheres[] = compact('type', 'column', 'values', 'boolean');
return $this;
}
/**
* Add an "or where in" clause to the query.
*
* @param string $column
* @param mixed $values
* @return $this
*/
public function orWhereIn(string $column, mixed $values): self
{
return $this->whereIn($column, $values, 'or');
}
/**
* Add a "where not in" clause to the query.
*
* @param string $column
* @param mixed $values
* @param string $boolean
* @return $this
*/
public function whereNotIn(string $column, mixed $values, string $boolean = 'and'): self
{
return $this->whereIn($column, $values, $boolean, true);
}
/**
* Add an "or where not in" clause to the query.
*
* @param string $column
* @param mixed $values
* @return $this
*/
public function orWhereNotIn(string $column, mixed $values): self
{
return $this->whereNotIn($column, $values, 'or');
}
/**
* Add a where in subquery clause to the query.
*
* @param string $column
* @param \Closure|Builder $query
* @param string $boolean
* @param bool $not
* @return $this
*/
protected function whereInSubquery(string $column, \Closure|Builder $query, string $boolean, bool $not): self
{
$type = $not ? 'NotInSubquery' : 'InSubquery';
if ($query instanceof \Closure) {
$callback = $query;
$query = $this->newQuery();
$callback($query);
}
$this->wheres[] = compact('type', 'column', 'query', 'boolean');
return $this;
}
/**
* Add an "order by" clause to the query.
*
* @param string $column
* @param string $direction
* @return $this
*/
public function orderBy(string $column, string $direction = 'asc'): self
{
$this->orders[] = compact('column', 'direction');
return $this;
}
/**
* Add a "limit" clause to the query.
*
* @param int $value
* @return $this
*/
public function limit(int $value): self
{
$this->limit = $value;
return $this;
}
/**
* Add an "offset" clause to the query.
*
* @param int $value
* @return $this
*/
public function offset(int $value): self
{
$this->offset = $value;
return $this;
}
/**
* Set the "lock" value of the query.
*
* @param bool|string $value
* @return $this
*/
public function lock(bool|string $value = true): self
{
$this->lock = $value;
return $this;
}
/**
* Lock the selected rows in the database for updating.
*
* @return $this
*/
public function lockForUpdate(): self
{
return $this->lock(true);
}
/**
* Share lock the selected rows in the database.
*
* @return $this
*/
public function sharedLock(): self
{
return $this->lock(false);
}
/**
* Add a join clause to the query.
*
* @param string|\Closure|Builder $table
* @param string|null|callable $first
* @param string|null $operator
* @param string|null $second
* @param string $type
* @return $this
*/
public function join(string|\Closure|Builder $table, $first = null, ?string $operator = null, ?string $second = null, string $type = 'inner'): self
{
if ($table instanceof \Closure || $table instanceof Builder) {
return $this->joinSub($table, (string)$first, $operator, $second, $type);
}
$this->joins[] = compact('table', 'first', 'operator', 'second', 'type');
return $this;
}
/**
* Add a join clause with a subquery to the query.
*
* @param \Closure|Builder $query
* @param string $as
* @param string $first
* @param string $operator
* @param string $second
* @param string $type
* @return $this
*/
public function joinSub(\Closure|Builder $query, string $as, $first, ?string $operator = null, ?string $second = null, string $type = 'inner'): self
{
if ($query instanceof \Closure) {
$callback = $query;
$query = $this->newQuery();
$callback($query);
}
$this->joins[] = [
'table' => [$query, $as],
'first' => $first,
'operator' => $operator,
'second' => $second,
'type' => $type,
];
return $this;
}
/**
* Add a left join clause to the query.
*
* @param string|\Closure|Builder $table
* @param string|null|callable $first
* @param string|null $operator
* @param string|null $second
* @return $this
*/
public function leftJoin(string|\Closure|Builder $table, $first = null, ?string $operator = null, ?string $second = null): self
{
return $this->join($table, $first, $operator, $second, 'left');
}
/**
* Add a left join clause with a subquery to the query.
*
* @param \Closure|Builder $query
* @param string $as
* @param string $first
* @param string $operator
* @param string $second
* @return $this
*/
public function leftJoinSub(\Closure|Builder $query, string $as, $first, ?string $operator = null, ?string $second = null): self
{
return $this->joinSub($query, $as, $first, $operator, $second, 'left');
}
/**
* Add a group by clause to the query.
*
* @param array|string $groups
* @return $this
*/
public function groupBy(array|string $groups): self
{
$groups = is_array($groups) ? $groups : func_get_args();
$this->groups = array_merge($this->groups, $groups);
return $this;
}
/**
* Add a having clause to the query.
*
* @param string $column
* @param string $operator
* @param mixed $value
* @param string $boolean
* @return $this
*/
public function having(string $column, string $operator, mixed $value, string $boolean = 'and'): self
{
$this->havings[] = compact('column', 'operator', 'value', 'boolean');
$this->addBinding($value, 'having');
return $this;
}
/**
* Get the SQL representation of the query.
*
* @return string
*/
public function toSql(): string
{
$this->applyTenancy();
return $this->grammar->compileSelect($this);
}
/**
* Get the SQL representation and bindings of the query.
*
* @return array{sql: string, bindings: array}
*/
public function sql(): array
{
return [
'sql' => $this->toSql(),
'bindings' => $this->getBindings(),
];
}
/**
* Set the query bindings.
*
* @param array $bindings
* @param string $type
* @return $this
*/
public function setBindings(array $bindings, string $type = 'where'): self
{
if (!array_key_exists($type, $this->bindings)) {
$translator = $this->db->getContainer()->get(\Pairity\Contracts\Translation\TranslatorInterface::class);
throw new \InvalidArgumentException($translator->trans('error.invalid_binding_type', ['type' => $type]));
}
$this->bindings[$type] = $bindings;
return $this;
}
/**
* Add a binding to the query.
*
* @param mixed $value
* @param string $type
* @return $this
*/
public function addBinding(mixed $value, string $type = 'where'): self
{
if (!array_key_exists($type, $this->bindings)) {
$translator = $this->db->getContainer()->get(\Pairity\Contracts\Translation\TranslatorInterface::class);
throw new \InvalidArgumentException($translator->trans('error.invalid_binding_type', ['type' => $type]));
}
if (is_array($value)) {
$this->bindings[$type] = array_values(array_merge($this->bindings[$type], $value));
} else {
$this->bindings[$type][] = $value;
}
return $this;
}
/**
* Get the current query bindings in a flattened array.
*
* @return array
*/
public function getBindings(): array
{
$flattened = [];
// Add bindings from subquery in FROM
if (is_array($this->from) && $this->from[0] instanceof Builder) {
$flattened = array_merge($flattened, $this->from[0]->getBindings());
}
// Add bindings from other components
foreach ($this->bindings as $type => $values) {
if ($type === 'select') {
$flattened = array_merge($flattened, $this->getSelectBindings());
} elseif ($type === 'join') {
$flattened = array_merge($flattened, $this->getJoinBindings());
} elseif ($type === 'where') {
$flattened = array_merge($flattened, $this->getWhereBindings());
} elseif ($type === 'union') {
// Unions already have their bindings added via addBinding in union()
// But for nested queries, it's safer to flatten here if we were storing Builders
$flattened = array_merge($flattened, $values);
} else {
$flattened = array_merge($flattened, $values);
}
}
return $flattened;
}
/**
* Get the bindings for the select clause, including subqueries.
*
* @return array
*/
protected function getSelectBindings(): array
{
$bindings = [];
foreach ((array) ($this->columns ?? []) as $column) {
if (is_array($column) && $column[0] instanceof Builder) {
$bindings = array_merge($bindings, $column[0]->getBindings());
}
}
return $bindings;
}
/**
* Get the bindings for the joins, including subqueries.
*
* @return array
*/
protected function getJoinBindings(): array
{
$bindings = [];
foreach ($this->joins as $join) {
if (is_array($join['table']) && $join['table'][0] instanceof Builder) {
$bindings = array_merge($bindings, $join['table'][0]->getBindings());
}
}
return $bindings;
}
/**
* Get the bindings for the where clauses, including subqueries.
*
* @return array
*/
protected function getWhereBindings(): array
{
$bindings = [];
foreach ($this->wheres as $where) {
if (isset($where['query']) && $where['query'] instanceof Builder) {
$bindings = array_merge($bindings, $where['query']->getBindings());
} elseif (isset($where['values'])) {
$bindings = array_merge($bindings, (array) $where['values']);
} elseif (isset($where['value'])) {
$bindings[] = $where['value'];
}
}
return $bindings;
}
/**
* Set the aggregate function and column for the query.
*
* @param string $function
* @param array $columns
* @return $this
*/
public function aggregate(string $function, array $columns = ['*']): self
{
$this->aggregate = compact('function', 'columns');
if (empty($this->columns)) {
$this->select($columns);
}
return $this;
}
/**
* Handle dynamic calls to the query builder.
*
* @param string $method
* @param array $parameters
* @return mixed
* @throws \BadMethodCallException
*/
public function __call(string $method, array $parameters)
{
if ($this->dao && method_exists($this->dao, $scope = 'scope' . ucfirst($method))) {
return $this->dao->$scope($this, ...$parameters) ?? $this;
}
$translator = $this->db->getContainer()->get(\Pairity\Contracts\Translation\TranslatorInterface::class);
throw new \BadMethodCallException($translator->trans('error.method_not_found', ['method' => $method]));
}
/**
* Set the DTO class and DAO for the query.
*
* @param string $dtoClass
* @param object $dao
* @return $this
*/
public function setModel(string $dtoClass, object $dao): self
{
$this->dtoClass = $dtoClass;
$this->dao = $dao;
return $this;
}
/**
* Execute the query and get the first result.
*
* @param array|string $columns
* @return object|null
*/
public function first(array|string $columns = ['*']): ?object
{
$results = $this->limit(1)->get($columns);
return $results[0] ?? null;
}
/**
* Update or insert records.
*
* @param array $values
* @param array $uniqueBy
* @param array|null $update
* @return int
*/
public function upsert(array $values, array $uniqueBy, ?array $update = null): int
{
if (empty($values)) {
return 0;
}
if (!is_array(reset($values))) {
$values = [$values];
}
if (is_null($update)) {
$update = array_keys(reset($values));
}
$bindings = $this->getUpsertBindings($values, $update);
return $this->connection->execute(
$this->grammar->compileUpsert($this, $values, $uniqueBy, $update),
$bindings
);
}
/**
* Get the bindings for an upsert statement.
*
* @param array $values
* @param array $update
* @return array
*/
protected function getUpsertBindings(array $values, array $update): array
{
$bindings = [];
foreach ($values as $record) {
foreach ($record as $value) {
$bindings[] = $value;
}
}
foreach ($update as $key => $value) {
if (!is_numeric($key)) {
$bindings[] = $value;
}
}
return $bindings;
}
/**
* Add a union statement to the query.
*
* @param Builder|\Closure $query
* @param bool $all
* @return $this
*/
public function union(Builder|\Closure $query, bool $all = false): self
{
if ($query instanceof \Closure) {
$callback = $query;
$query = $this->newQuery();
$callback($query);
}
$this->unions[] = compact('query', 'all');
$this->addBinding($query->getBindings(), 'union');
return $this;
}
/**
* Add a union all statement to the query.
*
* @param Builder|\Closure $query
* @return $this
*/
public function unionAll(Builder|\Closure $query): self
{
return $this->union($query, true);
}
/**
* Indicate that the query results should be cached.
*
* @param int $seconds
* @return $this
*/
public function remember(int $seconds): self
{
$this->cacheSeconds = $seconds;
return $this;
}
/**
* Execute the query as a "select" statement.
*
* @param array|string $columns
* @return array
*/
public function get(array|string $columns = ['*']): array
{
$this->applyTenancy();
if ($columns !== ['*']) {
$this->select($columns);
}
if ($this->cacheSeconds !== null) {
return $this->getCached($this->cacheSeconds);
}
return $this->executeSelect();
}
/**
* Execute the select statement.
*
* @return array
*/
protected function executeSelect(): array
{
$results = $this->connection->select($this->toSql(), $this->getBindings());
if (!$this->dtoClass || !$this->dao) {
return array_map(fn($row) => new QueryResult($row), $results);
}
$models = array_map(function ($row) {
$primaryKey = $this->dao->getPrimaryKey();
$id = $row[$primaryKey] ?? null;
if ($id && $cached = $this->dao->getIdentityMap()->get($this->dtoClass, $id)) {
return $cached;
}
$dto = $this->hydrate($row);
if ($id) {
$this->dao->getIdentityMap()->add($this->dtoClass, $id, $dto);
$this->db->unitOfWork()->track($dto, \Pairity\Database\UnitOfWork::STATE_CLEAN);
}
return $dto;
}, $results);
if (count($models) > 0 && !empty($this->eagerLoad)) {
$models = (new EagerLoader())->load($models, $this->eagerLoad);
}
return $models;
}
/**
* Get the cached results of the query.
*
* @param int $seconds
* @return array
*/
protected function getCached(int $seconds): array
{
$cache = $this->db->getContainer()->get(\Psr\SimpleCache\CacheInterface::class);
$key = 'query.' . md5($this->toSql() . serialize($this->getBindings()));
if ($cache->has($key)) {
return $cache->get($key);
}
$results = $this->executeSelect();
$cache->set($key, $results, $seconds);
return $results;
}
/**
* Hydrate a single row into a DTO.
*
* @param array $row
* @return object
*/
protected function hydrate(array $row): object
{
$dto = new $this->dtoClass();
if ($this->dao) {
$dto->setDao($this->dao);
if ($hydrator = $this->dao->getHydrator()) {
$hydrator->hydrate($row, $dto);
} else {
// Fallback to constructor if no hydrator
$dto = new $this->dtoClass($row);
$dto->setDao($this->dao);
}
} else {
// Fallback to constructor if no hydrator
$dto = new $this->dtoClass($row);
}
return $dto;
}
/**
* Retrieve the "count" result of the query.
*
* @param string $columns
* @return int
*/
public function count(string $columns = '*'): int
{
$backupModel = $this->dtoClass;
$backupDao = $this->dao;
$this->dtoClass = null;
$this->dao = null;
$results = $this->aggregate('count', [$columns])->get();
$this->dtoClass = $backupModel;
$this->dao = $backupDao;
return (int) ($results[0]->aggregate ?? 0);
}
/**
* Paginate the given query.
*
* @param int $perPage
* @param int $currentPage
* @return Paginator
*/
public function paginate(int $perPage = 15, int $currentPage = 1): Paginator
{
$total = $this->count();
// Use a new instance for the results to avoid conflicting with aggregate state
$itemsBuilder = clone $this;
$itemsBuilder->aggregate = null;
$items = $itemsBuilder->offset(($currentPage - 1) * $perPage)
->limit($perPage)
->get();
return new Paginator($items, $total, $perPage, $currentPage);
}
/**
* Update records in the database.
*
* @param array $values
* @return int
* @throws DatabaseException
*/
public function update(array $values): int
{
$this->applyTenancy();
$this->ensureConstrained('update');
$this->addBinding(array_values($values), 'update');
$sql = $this->grammar->compileUpdate($this, $values);
return $this->connection->execute($sql, $this->getBindings());
}
/**
* Delete records from the database.
*
* @return int
* @throws DatabaseException
*/
public function delete(): int
{
$this->applyTenancy();
$this->ensureConstrained('delete');
$sql = $this->grammar->compileDelete($this);
return $this->connection->execute($sql, $this->getBindings());
}
/**
* Disable multi-tenancy for the query.
*
* @return $this
*/
public function withoutTenancy(): self
{
$this->withoutTenancy = true;
return $this;
}
/**
* Apply multi-tenancy scope to the query if enabled.
*
* @return void
*/
protected function applyTenancy(): void
{
if ($this->withoutTenancy || !$this->dao) {
return;
}
if ($this->dao->getOption('tenancy', false)) {
(new \Pairity\Database\Query\Scopes\TenantScope())->apply($this);
}
}
/**
* Ensure the query is constrained if unconstrained queries are disabled.
*
* @param string $operation
* @return void
* @throws DatabaseException
*/
protected function ensureConstrained(string $operation): void
{
$config = $this->connection->getConfig();
$allowUnconstrained = $config['allow_unconstrained_queries'] ?? false;
if (!$allowUnconstrained && empty($this->wheres)) {
$translator = $this->db->getContainer()->get(\Pairity\Contracts\Translation\TranslatorInterface::class);
throw new DatabaseException(
$translator->trans('error.unconstrained_query', ['operation' => $operation]),
0,
null,
['operation' => $operation]
);
}
}
}