<?php

/**
 * The QueryBuilder class provides a fluent interface for building SQL queries.
 * This class allows you to construct complex SQL queries using a chainable method syntax,
 * making it easier to write and maintain database queries.
 *
 * @package    Framework
 * @subpackage Database\Query
 * @since      1.0.0
 */
namespace Kirki\Framework\Database\Query;

\defined('ABSPATH') || exit;
use BadMethodCallException;
use Closure;
use DateTimeInterface;
use Kirki\Framework\Collections\Collection;
use Kirki\Framework\Database\Concerns\ExecuteQueries;
use Kirki\Framework\Database\Concerns\RelationshipQueries;
use Kirki\Framework\Database\Connection\Connection;
use Kirki\Framework\Database\Query\Collection as QueryCollection;
use Kirki\Framework\Database\Query\EagerLoader;
use Kirki\Framework\Database\Query\Relations\Relation;
use Kirki\Framework\Exceptions\ModelNotFoundException;
use Kirki\Framework\Exceptions\MultipleRecordsFoundException;
use Kirki\Framework\Exceptions\RecordNotFoundException;
use Kirki\Framework\Exceptions\UniqueConstraintViolationException;
use Kirki\Framework\Supports\Arr;
use Kirki\Framework\Supports\Str;
use Kirki\Framework\Supports\Traits\Conditionable;
use Kirki\Framework\Supports\Traits\Macroable;
use InvalidArgumentException;
use function Kirki\Framework\collection;
use function Kirki\Framework\Polyfill\array_first;
use function Kirki\Framework\Polyfill\array_last;
use function Kirki\Framework\Polyfill\str_contains;
use function Kirki\Framework\tap;
use function Kirki\Framework\value;
/**
 * The QueryBuilder class provides a fluent interface for building SQL queries.
 *
 * @method QueryBuilder when($value, callable $callback, $default)
 * @method QueryBuilder unless($value, callable $callback, $default)
 */
class QueryBuilder
{
    use ExecuteQueries, RelationshipQueries, Conditionable, Macroable {
        __call as macroCall;
    }
    /**
     * Default pagination limit
     *
     * @var int
     */
    public const PAGINATION_LIMIT = 20;
    /**
     * The database connection instance used to execute queries.
     *
     * @var \Framework\Database\Connection\Connection
     *
     * @since 1.0.0
     */
    public $connection;
    /**
     * The query compiler instance used to compile the query.
     *
     * @var \Framework\Database\Query\QueryCompiler
     *
     * @since 1.0.0
     */
    public $compiler;
    /**
     * The name of the table to perform queries against.
     *
     * @var string|null
     *
     * @since 1.0.0
     */
    public $from;
    /**
     * The columns to be selected in the query. Defaults to all columns.
     *
     * @var array|null
     *
     * @since 1.0.0
     */
    public $columns;
    /**
     * The where conditions applied to the query for filtering results.
     *
     * @var array
     *
     * @since 1.0.0
     */
    public $wheres = [];
    /**
     * The bindings for prepared statements to prevent SQL injection.
     *
     * @var array
     *
     * @since 1.0.0
     */
    public $bindings = ['select' => [], 'from' => [], 'join' => [], 'where' => [], 'group_by' => [], 'having' => [], 'order' => []];
    /**
     * List of valid SQL operators supported by the query builder.
     *
     * @var array
     *
     * @since 1.0.0
     */
    public $operators = ['=', '<', '>', '<=', '>=', '<>', '!=', '<=>', 'like', 'like binary', 'not like', 'ilike', '&', '|', '^', '<<', '>>', '&~', 'is', 'is not', 'rlike', 'not rlike', 'regexp', 'not regexp', '~', '~*', '!~', '!~*', 'similar to', 'not similar to', 'not ilike', '~~*', '!~~*'];
    /**
     * The join clauses to include related tables in the query.
     *
     * @var array|null
     *
     * @since 1.0.0
     */
    public $joins;
    /**
     * The columns used to group the query results.
     *
     * @var array|null
     *
     * @since 1.0.0
     */
    public $groups;
    /**
     * The having conditions applied after grouping results.
     *
     * @var array|null
     *
     * @since 1.0.0
     */
    public $havings;
    /**
     * The order by clauses to sort the query results.
     *
     * @var array|null
     *
     * @since 1.0.0
     */
    public $orders;
    /**
     * The aggregate function and columns to be used in the query.
     *
     * @var array|null
     *
     * @since 1.0.0
     */
    public $aggregate;
    /**
     * The maximum number of records to return from the query.
     *
     * @var int|null
     *
     * @since 1.0.0
     */
    public $limit;
    /**
     * The number of records to skip before starting to return results.
     *
     * @var int|null
     *
     * @since 1.0.0
     */
    public $offset;
    /**
     * The relationships to eager load with the query results.
     *
     * @var array
     *
     * @since 1.0.0
     */
    public $with = [];
    /**
     * The relationships that have been eager loaded.
     *
     * @var array
     *
     * @since 1.0.0
     */
    protected $eager_loaded_relations = [];
    /**
     * The relationships to count with the query results.
     *
     * @var array
     *
     * @since 1.0.0
     */
    public $with_count = [];
    /**
     * The model class used to hydrate the query results.
     *
     * @var Model|null
     *
     * @since 1.0.0
     */
    public $model = null;
    /**
     * The Wordpress Database connection instance.
     *
     * @var \wpdb|null
     *
     * @since 1.0.0
     */
    public $db = null;
    /**
     * The distinct flag to select distinct rows from the query.
     *
     * @var bool|array
     *
     * @since 1.0.0
     */
    public $distinct = \false;
    /**
     * The flag to enable or disable resolving relations of the results.
     *
     * @var bool
     *
     * @since 1.0.0
     */
    protected static $should_resolve_relations = \true;
    /**
     * Initialize a new QueryBuilder instance with database connection, table name and model class
     *
     * @param Connection $connection The database connection instance
     * @param QueryCompiler|null $compiler The compiler instance to use
     * @param Model|null $model The model class for hydrating results
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function __construct(Connection $connection, $compiler = null, $model = null)
    {
        $this->connection = $connection;
        $this->compiler = $compiler ?? $this->connection->get_query_compiler();
        $this->model = $model;
        $this->db = $connection->get_db();
        $this->sync_with_model();
    }
    /**
     * Sync the query builder with the model
     *
     * @return void
     *
     * @since 1.0.0
     */
    protected function sync_with_model()
    {
        if ($this->model) {
            $table_name = $this->model->get_table();
            $this->from($table_name);
        }
    }
    /**
     * Set the model.
     *
     * @param Model $model The model instance.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function set_model(Model $model)
    {
        $this->model = $model;
        $this->sync_with_model();
    }
    /**
     * Get the model instance
     *
     * @return Model|null The model instance
     *
     * @since 1.0.0
     */
    public function get_model()
    {
        return $this->model;
    }
    /**
     * Get the query compiler instance
     *
     * @return QueryCompiler
     *
     * @since 1.0.0
     */
    public function get_compiler()
    {
        return $this->connection->get_query_compiler();
    }
    /**
     * Get the limit for the query
     *
     * @return int|null The limit for the query
     *
     * @since 1.0.0
     */
    public function get_limit()
    {
        return !\is_null($this->limit) ? (int) $this->limit : null;
    }
    /**
     * Get the offset for the query
     *
     * @return int|null The offset for the query
     *
     * @since 1.0.0
     */
    public function get_offset()
    {
        return !\is_null($this->offset) ? (int) $this->offset : null;
    }
    /**
     * Default key name.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function default_key_name()
    {
        if ($this->model) {
            return $this->model->get_primary_key();
        }
        return 'id';
    }
    /**
     * Set the table name for the query
     *
     * @param string|Expression|QueryBuilder|Model $table The table name to query
     * @param string|null $as The alias of the table.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function from($table, $as = null)
    {
        if (\is_subclass_of($table, Model::class)) {
            $table = (new $table())->get_table();
        }
        if ($this->is_queryable($table)) {
            return $this->from_subquery($table, $as);
        }
        $this->from = $as ? \sprintf('%s as %s', $table, $as) : $table;
        return $this;
    }
    /**
     * Set the table source using a subquery
     *
     * @param QueryBuilder|Closure $query The subquery to set as the table source
     * @param string $as The alias for the subquery
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function from_subquery($query, $as)
    {
        [$query, $bindings] = $this->create_subquery($query);
        return $this->from_raw(\sprintf('(%s) as %s', $query, $this->get_compiler()->wrap($as)), $bindings);
    }
    /**
     * Set the table source using a raw expression
     *
     * @param string $expression The raw SQL expression to set as the table source
     * @param array $bindings The bindings for the raw expression
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function from_raw($expression, array $bindings = [])
    {
        $this->from = new Expression($expression);
        $this->add_bindings($bindings, 'from');
        return $this;
    }
    /**
     * Set the bindings for the query
     *
     * @param array $bindings The bindings to set
     * @param string $type The type of binding to set
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @throws \InvalidArgumentException
     *
     * @since 1.0.0
     */
    public function set_bindings(array $bindings, $type = 'where')
    {
        if (!\array_key_exists($type, $this->bindings)) {
            throw new InvalidArgumentException("Invalid binding type: {$type}");
        }
        $this->bindings[$type] = $bindings;
        return $this;
    }
    /**
     * Add bindings to the query
     *
     * @param mixed $value The value to add to the bindings
     * @param string $type The type of binding to add
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @throws \InvalidArgumentException
     *
     * @since 1.0.0
     */
    public function add_bindings($value, $type = 'where')
    {
        $type = \strtolower($type);
        if (!\array_key_exists($type, $this->bindings)) {
            throw new InvalidArgumentException(\sprintf('Invalid binding type: %s', $type));
        }
        if (\is_array($value)) {
            $this->bindings[$type] = \array_values(\array_merge($this->bindings[$type], $value));
        } else {
            $this->bindings[$type][] = $value;
        }
        return $this;
    }
    /**
     * Set the columns to be selected in the query
     *
     * This method allows you to specify which columns should be returned from the database query.
     * You can pass either an array of column names or multiple string arguments. If no columns
     * are specified, it defaults to selecting all columns using the wildcard (*).
     *
     * @param array|string $columns The columns to select, defaults to all columns
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function select($columns = ['*'])
    {
        $this->columns = [];
        $this->bindings['select'] = [];
        $columns = \is_array($columns) ? $columns : \func_get_args();
        foreach ($columns as $as => $column) {
            if (\is_string($as) && $this->is_queryable($column)) {
                $this->select_subquery($column, $as);
            } else {
                $this->columns[] = $column;
            }
        }
        return $this;
    }
    /**
     * Add a raw SELECT clause to the query
     *
     * @param string $expression The raw SQL expression to select
     * @param array $bindings Parameter values to bind to the query
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function select_raw($expression, array $bindings = [])
    {
        $this->add_select(new Expression($expression));
        if ($bindings) {
            $this->add_bindings($bindings, 'select');
        }
        return $this;
    }
    /**
     * Add a subquery to the SELECT clause
     *
     * @param Closure|QueryBuilder $query The subquery to add
     * @param string $as The alias for the subquery
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function select_subquery($query, $as)
    {
        [$query, $bindings] = $this->create_subquery($query);
        return $this->select_raw(\sprintf('(%s) as %s', $query, $this->get_compiler()->wrap($as)), $bindings);
    }
    /**
     * Add a column to the SELECT clause
     *
     * @param string|Expression|array $columns The column to add
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function add_select($columns)
    {
        $columns = \is_array($columns) ? $columns : \func_get_args();
        foreach ($columns as $as => $column) {
            if (\is_string($as) && $this->is_queryable($column)) {
                if (\is_null($this->columns)) {
                    $this->select(\sprintf('%s.*', $this->from));
                }
                $this->select_subquery($column, $as);
            } else {
                if (\is_array($this->columns) && \in_array($column, $this->columns, \true)) {
                    continue;
                }
                $this->columns[] = $column;
            }
        }
        return $this;
    }
    /**
     * Set the distinct flag for the query
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function distinct()
    {
        $columns = \func_get_args();
        if (\count($columns) > 0) {
            $this->distinct = \is_array($columns[0]) || \is_bool($columns[0]) ? $columns[0] : $columns;
        } else {
            $this->distinct = \true;
        }
        return $this;
    }
    /**
     * Check if the query has any results
     *
     * @return bool True if the query has any results, false otherwise
     *
     * @since 1.0.0
     */
    public function exists()
    {
        $results = $this->connection->select($this->compiler->compile_exists($this), $this->get_bindings());
        if (isset($results[0])) {
            $results = (array) $results[0];
            return (bool) $results['exists'];
        }
        return \false;
    }
    /**
     * Check if the query does not have any results
     *
     * @return bool True if the query does not have any results, false otherwise
     *
     * @since 1.0.0
     */
    public function does_not_exists()
    {
        return !$this->exists();
    }
    /**
     * Get the value of a single column from the first result
     *
     * @param string $column The column name to get the value from
     *
     * @return mixed The value of the column
     *
     * @since 1.0.0
     */
    public function value($column)
    {
        $result = (array) $this->first([$column]);
        return \count($result) > 0 ? array_first($result) : null;
    }
    /**
     * Add a WHERE clause to the query with AND condition
     *
     * This method adds a basic WHERE condition to the query using the AND boolean operator.
     * If only two parameters are provided, the operator defaults to '=' for equality comparison.
     * The condition is added to the existing WHERE conditions array and the value is bound
     * to prevent SQL injection attacks.
     *
     * @param string $column The column name to filter
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     * @param mixed $boolean The boolean.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where($column, $operator = null, $value = null, $boolean = 'and')
    {
        if (\is_array($column)) {
            return $this->add_array_of_wheres($column, $boolean);
        }
        [$value, $operator] = $this->prepare_value_and_operator($value, $operator, \func_num_args() === 2);
        if ($column instanceof Closure && \is_null($operator)) {
            return $this->where_nested($column, $boolean);
        }
        if ($this->is_queryable($column) && !\is_null($operator)) {
            [$sub_query, $bindings] = $this->create_subquery($column);
            return $this->add_bindings($bindings, 'where')->where(new Expression('(' . $sub_query . ')'), $operator, $value, $boolean);
        }
        if ($this->is_invalid_operator($operator)) {
            [$value, $operator] = [$operator, '='];
        }
        if ($this->is_queryable($value)) {
            return $this->where_subquery($column, $operator, $value, $boolean);
        }
        if (\is_null($value)) {
            return $this->where_null($column, $boolean, $operator !== '=');
        }
        $type = 'basic';
        $this->wheres[] = \compact('type', 'column', 'operator', 'value', 'boolean');
        $this->add_bindings($value, 'where');
        return $this;
    }
    /**
     * Add a WHERE clause to the query with a subquery
     *
     * @param string $column The column name to filter
     * @param string $operator The comparison operator (=, >, <, etc.)
     * @param Closure $callback The subquery to add
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    protected function where_subquery($column, $operator, $callback, $boolean)
    {
        $type = 'subquery';
        if ($callback instanceof Closure) {
            $callback($query = $this->for_subquery());
        } else {
            $query = $callback;
        }
        $this->wheres[] = \compact('type', 'column', 'operator', 'query', 'boolean');
        $this->add_bindings($query->get_bindings(), 'where');
        return $this;
    }
    /**
     * Add a WHERE clause to the query with a raw SQL expression
     *
     * @param string $sql The raw SQL expression to add
     * @param array $bindings The bindings for the raw expression
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_raw($sql, array $bindings = [], $boolean = 'and')
    {
        $type = 'raw';
        $this->wheres[] = \compact('type', 'sql', 'boolean');
        $this->add_bindings($bindings, 'where');
        return $this;
    }
    /**
     * Add a WHERE clause to the query with a raw SQL expression and OR condition
     *
     * @param string $sql The raw SQL expression to add
     * @param array $bindings The bindings for the raw expression
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_raw($sql, array $bindings = [])
    {
        return $this->where_raw($sql, $bindings, 'or');
    }
    /**
     * Add a WHERE clause to the query with a LIKE condition
     *
     * @param string $column The column name to filter
     * @param string $value The value to compare against
     * @param bool $case_sensitive Whether the comparison should be case-sensitive
     * @param string $boolean The boolean operator (and, or)
     * @param bool $not Whether the condition should be negated
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_like($column, $value, $case_sensitive = \false, $boolean = 'and', $not = \false)
    {
        $type = 'like';
        $this->wheres[] = \compact('type', 'column', 'value', 'case_sensitive', 'boolean', 'not');
        $this->add_bindings($value, 'where');
        return $this;
    }
    /**
     * Add a WHERE clause to the query with a LIKE condition and OR condition
     *
     * @param string $column The column name to filter
     * @param string $value The value to compare against
     * @param bool $case_sensitive Whether the comparison should be case-sensitive
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_like($column, $value, $case_sensitive = \false)
    {
        return $this->where_like($column, $value, $case_sensitive, 'or', \false);
    }
    /**
     * Add a WHERE clause to the query with a LIKE condition and NOT condition
     *
     * @param string $column The column name to filter
     * @param string $value The value to compare against
     * @param bool $case_sensitive Whether the comparison should be case-sensitive
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_not_like($column, $value, $case_sensitive = \false, $boolean = 'and')
    {
        return $this->where_like($column, $value, $case_sensitive, $boolean, \true);
    }
    /**
     * Add a WHERE clause to the query with a LIKE condition and NOT condition and OR condition
     *
     * @param string $column The column name to filter
     * @param string $value The value to compare against
     * @param bool $case_sensitive Whether the comparison should be case-sensitive
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_not_like($column, $value, $case_sensitive = \false)
    {
        return $this->where_not_like($column, $value, $case_sensitive, 'or');
    }
    /**
     * Add a WHERE clause to the query with OR condition
     *
     * This method adds a basic WHERE condition to the query using the OR boolean operator.
     * It functions identically to the where() method but uses OR instead of AND to combine
     * with previous conditions. The operator defaults to '=' if not specified.
     *
     * @param string $column The column name to filter
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where($column, $operator = null, $value = null)
    {
        [$value, $operator] = $this->prepare_value_and_operator($value, $operator, \func_num_args() === 2);
        return $this->where($column, $operator, $value, 'or');
    }
    /**
     * Add a WHERE clause to the query with a NOT condition
     *
     * @param string $column The column name to filter
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_not($column, $operator = null, $value = null, $boolean = 'and')
    {
        if (\is_array($column)) {
            return $this->where_nested(function ($query) use($column, $operator, $value, $boolean) {
                $query->where($column, $operator, $value, $boolean);
            }, $boolean . ' not');
        }
        return $this->where($column, $operator, $value, $boolean . ' not');
    }
    /**
     * Add a WHERE clause to the query with a NOT condition and OR condition
     *
     * @param string $column The column name to filter
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_not($column, $operator = null, $value = null)
    {
        return $this->where_not($column, $operator, $value, 'or');
    }
    /**
     * Add a WHERE IN clause to filter records where column value is in the given array
     *
     * This method creates a WHERE IN condition that matches records where the specified
     * column's value exists within the provided array of values. All values in the array
     * are properly bound to prevent SQL injection. This is useful for filtering records
     * that match any of multiple possible values.
     *
     * @param string $column The column name to check
     * @param array|Closure $values The array of values to match against
     * @param mixed $boolean The boolean.
     * @param mixed $not The not.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @throws \InvalidArgumentException
     *
     * @since 1.0.0
     */
    public function where_in($column, $values, $boolean = 'and', $not = \false)
    {
        $type = $not ? 'not_in' : 'in';
        if ($this->is_queryable($values)) {
            [$query, $bindings] = $this->create_subquery($values);
            $values = [new Expression($query)];
            $this->add_bindings($bindings, 'where');
        }
        $this->wheres[] = \compact('type', 'column', 'values', 'boolean');
        if (\count($values) !== \count(Arr::flatten($values, 1))) {
            throw new InvalidArgumentException('Nested array of values is not allowed');
        }
        $this->add_bindings($this->clean_bindings($values), 'where');
        return $this;
    }
    /**
     * Add a WHERE IN clause to filter records where column value is in the given array and OR condition
     *
     * @param string $column The column name to check
     * @param array $values The array of values to match against
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_in($column, $values)
    {
        return $this->where_in($column, $values, 'or');
    }
    /**
     * Add a WHERE NOT IN clause to filter records where column value is not in the given array
     *
     * This method creates a WHERE NOT IN condition that matches records where the specified
     * column's value does not exist within the provided array of values. It's the inverse
     * of the whereIn() method and is useful for excluding records that match certain values.
     *
     * @param string $column The column name to check
     * @param array $values The array of values to exclude
     * @param mixed $boolean The boolean.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_not_in($column, $values, $boolean = 'and')
    {
        return $this->where_in($column, $values, $boolean, \true);
    }
    /**
     * Add a WHERE NOT IN clause to filter records where column value is not in the given array and OR condition
     *
     * @param string $column The column name to check
     * @param array $values The array of values to exclude
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_not_in($column, $values)
    {
        return $this->where_not_in($column, $values, 'or');
    }
    /**
     * Add a WHERE BETWEEN clause to filter records where column value is between two values
     *
     * This method creates a WHERE BETWEEN condition that matches records where the specified
     * column's value falls within the range defined by the two provided values (inclusive).
     * The values array must contain exactly two elements: the start and end of the range.
     *
     * @param string $column The column name to check
     * @param array $values Array containing the start and end values for the range
     * @param mixed $boolean The boolean.
     * @param mixed $not The not.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_between($column, $values, $boolean = 'and', $not = \false)
    {
        $type = 'between';
        $this->wheres[] = \compact('type', 'column', 'values', 'boolean', 'not');
        $this->add_bindings(\array_slice($this->clean_bindings(Arr::flatten($values)), 0, 2), 'where');
        return $this;
    }
    /**
     * Add a WHERE BETWEEN clause to filter records where column value is between two values and OR condition
     *
     * @param string $column The column name to check
     * @param array $values Array containing the start and end values for the range to include
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_between($column, $values)
    {
        return $this->where_between($column, $values, 'or');
    }
    /**
     * Add a WHERE NOT BETWEEN clause to filter records where column value is not between two values
     *
     * This method creates a WHERE NOT BETWEEN condition that matches records where the specified
     * column's value falls outside the range defined by the two provided values. It's the inverse
     * of the whereBetween() method and excludes records within the specified range.
     *
     * @param string $column The column name to check
     * @param array $values Array containing the start and end values for the range to exclude
     * @param mixed $boolean The boolean.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_not_between($column, $values, $boolean = 'and')
    {
        return $this->where_between($column, $values, $boolean, \true);
    }
    /**
     * Add a WHERE NOT BETWEEN clause to filter records where column value is not between two values and OR condition
     *
     * @param string $column The column name to check
     * @param array $values Array containing the start and end values for the range to exclude
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_not_between($column, $values)
    {
        return $this->where_not_between($column, $values, 'or');
    }
    /**
     * Add a WHERE IS NULL clause to filter records where column value is null
     *
     * This method creates a WHERE IS NULL condition that matches records where the specified
     * column contains a null value. This is useful for finding records with missing or
     * undefined data in specific columns.
     *
     * @param string $columns The column name to check for null values
     * @param mixed $boolean The boolean.
     * @param mixed $not The not.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_null($columns, $boolean = 'and', $not = \false)
    {
        $type = $not ? 'not_null' : 'null';
        foreach (Arr::wrap($columns) as $column) {
            $this->wheres[] = \compact('type', 'column', 'boolean');
        }
        return $this;
    }
    /**
     * Add a WHERE IS NULL clause to filter records where column value is null and OR condition
     *
     * @param string $column The column name to check for null values
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_null($column)
    {
        return $this->where_null($column, 'or');
    }
    /**
     * Add a WHERE IS NOT NULL clause to filter records where column value is not null
     *
     * This method creates a WHERE IS NOT NULL condition that matches records where the specified
     * column contains any non-null value. It's useful for ensuring that records have actual
     * data in specific columns and excluding records with missing values.
     *
     * @param string $column The column name to check for non-null values
     * @param mixed $boolean The boolean.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_not_null($column, $boolean = 'and')
    {
        return $this->where_null($column, $boolean, \true);
    }
    /**
     * Add a WHERE IS NOT NULL clause to filter records where column value is not null and OR condition
     *
     * @param string $column The column name to check for non-null values
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_not_null($column)
    {
        return $this->where_not_null($column, 'or');
    }
    /**
     * Add a WHERE DATE clause to filter records where column value is a date
     *
     * @param string $column The column name to check for date values
     * @param string $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_date($column, $operator, $value = null, $boolean = 'and')
    {
        [$value, $operator] = $this->prepare_value_and_operator($value, $operator, \func_num_args() === 2);
        if ($this->is_invalid_operator($operator)) {
            [$value, $operator] = [$operator, '='];
        }
        $value = $this->flatten_value($value);
        if ($value instanceof DateTimeInterface) {
            $value = $value->format('Y-m-d');
        }
        return $this->add_date_based_where('date', $column, $operator, $value, $boolean);
    }
    /**
     * Add a WHERE DATE clause to filter records where column value is a date and OR condition
     *
     * @param string $column The column name to check for date values
     * @param string $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_date($column, $operator, $value = null)
    {
        [$value, $operator] = $this->prepare_value_and_operator($value, $operator, \func_num_args() === 2);
        return $this->where_date($column, $operator, $value, 'or');
    }
    /**
     * Add a WHERE DATE clause to filter records where column value is a date
     *
     * @param string $type The type of date comparison (date or time)
     * @param string $column The column name to check for date values
     * @param string $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    protected function add_date_based_where($type, $column, $operator, $value, $boolean)
    {
        $this->wheres[] = \compact('column', 'type', 'boolean', 'operator', 'value');
        if (!$value instanceof Expression) {
            $this->add_bindings($value, 'where');
        }
        return $this;
    }
    /**
     * Add a WHERE TIME clause to filter records where column value is a time
     *
     * @param string $column The column name to check for time values
     * @param string $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_time($column, $operator, $value = null, $boolean = 'and')
    {
        [$value, $operator] = $this->prepare_value_and_operator($value, $operator, \func_num_args() === 2);
        if ($this->is_invalid_operator($operator)) {
            [$value, $operator] = [$operator, '='];
        }
        $value = $this->flatten_value($value);
        if ($value instanceof DateTimeInterface) {
            $value = $value->format('H:i:s');
        }
        return $this->add_date_based_where('time', $column, $operator, $value, $boolean);
    }
    /**
     * Add a WHERE TIME clause to filter records where column value is a time and OR condition
     *
     * @param string $column The column name to check for time values
     * @param string $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_time($column, $operator, $value = null)
    {
        [$value, $operator] = $this->prepare_value_and_operator($value, $operator, \func_num_args() === 2);
        return $this->where_time($column, $operator, $value, 'or');
    }
    /**
     * Add a WHERE COLUMN clause to filter records where two columns are equal
     *
     * This method creates a WHERE COLUMN condition that matches records where the
     * values of the two specified columns are equal. It's useful for comparing
     * values between different columns in the same table.
     *
     * @param string $first The first column to compare
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param string|null $second The second column to compare
     * @param string $boolean The boolean operator to combine with previous conditions
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_column($first, $operator = null, $second = null, $boolean = 'and')
    {
        if (\is_array($first)) {
            return $this->add_array_of_wheres($first, $boolean, 'where_column');
        }
        if ($this->is_invalid_operator($operator)) {
            [$second, $operator] = [$operator, '='];
        }
        $type = 'column';
        $this->wheres[] = \compact('type', 'first', 'operator', 'second', 'boolean');
        return $this;
    }
    /**
     * Add a WHERE COLUMN clause to filter records where two columns are equal and OR condition
     *
     * @param string $first The first column to compare
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param string|null $second The second column to compare
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_column($first, $operator = null, $second = null)
    {
        return $this->where_column($first, $operator, $second, 'or');
    }
    /**
     * Add a WHERE ALL clause to filter records where all columns match the given value
     *
     * @param array $columns The columns to check for matching values
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     * @param string $boolean The boolean operator to combine with previous conditions
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_all($columns, $operator = null, $value = null, $boolean = 'and')
    {
        [$value, $operator] = $this->prepare_value_and_operator($value, $operator, \func_num_args() === 2);
        $this->where_nested(function ($query) use($columns, $operator, $value) {
            foreach ($columns as $column) {
                $query->where($column, $operator, $value, 'and');
            }
        }, $boolean);
        return $this;
    }
    /**
     * Add a WHERE ALL clause to filter records where all columns match the given value and OR condition
     *
     * @param array $columns The columns to check for matching values
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_all($columns, $operator = null, $value = null)
    {
        return $this->where_all($columns, $operator, $value, 'or');
    }
    /**
     * Add a WHERE ANY clause to filter records where any column matches the given value
     *
     * @param array $columns The columns to check for matching values
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     * @param string $boolean The boolean operator to combine with previous conditions
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_any($columns, $operator = null, $value = null, $boolean = 'and')
    {
        [$value, $operator] = $this->prepare_value_and_operator($value, $operator, \func_num_args() === 2);
        $this->where_nested(function ($query) use($columns, $operator, $value) {
            foreach ($columns as $column) {
                $query->where($column, $operator, $value, 'or');
            }
        }, $boolean);
        return $this;
    }
    /**
     * Add a WHERE ANY clause to filter records where any column matches the given value and OR condition
     *
     * @param array $columns The columns to check for matching values
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_any($columns, $operator = null, $value = null)
    {
        return $this->where_any($columns, $operator, $value, 'or');
    }
    /**
     * Add a WHERE NONE clause to filter records where no column matches the given value
     *
     * @param array $columns The columns to check for matching values
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     * @param string $boolean The boolean operator to combine with previous conditions
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_none($columns, $operator = null, $value = null, $boolean = 'and')
    {
        return $this->where_any($columns, $operator, $value, $boolean . ' not');
    }
    /**
     * Add a WHERE NONE clause to filter records where no column matches the given value and OR condition
     *
     * @param array $columns The columns to check for matching values
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_none($columns, $operator = null, $value = null)
    {
        return $this->where_none($columns, $operator, $value, 'or');
    }
    /**
     * Merge an array of WHERE conditions into the query
     *
     * @param array $wheres The array of WHERE conditions to merge
     * @param array $bindings The array of bindings to merge
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function merge_wheres($wheres, $bindings)
    {
        $this->wheres = \array_merge($this->wheres, $wheres);
        $this->bindings['where'] = \array_values(\array_merge($this->bindings['where'], $bindings));
        return $this;
    }
    /**
     * Add an array of WHERE conditions to the query
     *
     * @param array $column The array of columns to add WHERE conditions for
     * @param string $boolean The boolean operator (and, or)
     * @param string $method The method to use for adding the conditions (where, or_where, etc.)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    protected function add_array_of_wheres(array $column, $boolean, $method = 'where')
    {
        return $this->where_nested(function ($query) use($column, $method, $boolean) {
            foreach ($column as $key => $value) {
                if (\is_numeric($key) && \is_array($value)) {
                    \call_user_func_array([$query, $method], \array_merge(\array_values($value), [$boolean]));
                } else {
                    $query->{$method}($key, '=', $value, $boolean);
                }
            }
        }, $boolean);
    }
    /**
     * Add a nested WHERE condition to the query
     *
     * @param Closure $callback The callback function to add the nested WHERE condition
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    protected function where_nested(Closure $callback, $boolean = 'and')
    {
        $callback($query = $this->for_nested_where());
        return $this->add_nested_where_query($query, $boolean);
    }
    /**
     * Add a nested WHERE condition to the query
     *
     * @param QueryBuilder $query The query builder instance to add the nested WHERE condition to
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    protected function add_nested_where_query($query, $boolean = 'and')
    {
        if (\count($query->wheres)) {
            $type = 'nested';
            $this->wheres[] = \compact('type', 'query', 'boolean');
            $this->add_bindings($query->get_binding('where'), 'where');
        }
        return $this;
    }
    /**
     * Create a new query builder instance for nested WHERE conditions
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    protected function for_nested_where()
    {
        return $this->new_query()->from($this->from);
    }
    /**
     * Add a WHERE EXISTS clause to the query
     *
     * @param Closure $callback The callback function to add the WHERE EXISTS condition
     * @param string $boolean The boolean operator (and, or)
     * @param bool $not Whether the condition should be negated
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_exists($callback, $boolean = 'and', $not = \false)
    {
        if ($callback instanceof Closure) {
            $query = $this->for_subquery();
            $callback($query);
        } else {
            $query = $callback;
        }
        return $this->add_where_exists_query($query, $boolean, $not);
    }
    /**
     * Add a WHERE EXISTS clause to the query and OR condition
     *
     * @param Closure $callback The callback function to add the WHERE EXISTS condition
     * @param bool $not Whether the condition should be negated
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_exists($callback, $not = \false)
    {
        return $this->where_exists($callback, 'or', $not);
    }
    /**
     * Add a WHERE EXISTS clause to the query and NOT condition
     *
     * @param Closure $callback The callback function to add the WHERE EXISTS condition
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function where_not_exists($callback, $boolean = 'and')
    {
        return $this->where_exists($callback, $boolean, \true);
    }
    /**
     * Add a WHERE EXISTS clause to the query and NOT condition and OR condition
     *
     * @param Closure $callback The callback function to add the WHERE EXISTS condition
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_where_not_exists($callback)
    {
        return $this->where_not_exists($callback, 'or');
    }
    /**
     * Add a WHERE EXISTS clause to the query
     *
     * @param QueryBuilder $query The query builder instance to add the WHERE EXISTS condition to
     * @param string $boolean The boolean operator (and, or)
     * @param bool $not Whether the condition should be negated
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    protected function add_where_exists_query(QueryBuilder $query, $boolean = 'and', $not = \false)
    {
        $type = $not ? 'not_exists' : 'exists';
        $this->wheres[] = \compact('type', 'query', 'boolean');
        $this->add_bindings($query->get_bindings(), 'where');
        return $this;
    }
    /**
     * Add a JOIN clause to the query for combining records from multiple tables
     *
     * This method adds a JOIN clause to combine records from multiple tables based on a
     * related column between them. If the operator is not specified, it defaults to '='.
     * The join type can be INNER, LEFT, or RIGHT, with INNER being the default.
     *
     * @param string $table The table to join with
     * @param string $first The first column in the join condition
     * @param string|null $operator The join operator (=, >, <, etc.)
     * @param string|null $second The second column in the join condition
     * @param string $type The type of join (INNER, LEFT, RIGHT)
     * @param mixed $where The where.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = \false)
    {
        $join = $this->new_join_clause($this, $type, $table);
        if ($first instanceof Closure) {
            $first($join);
            $this->joins[] = $join;
            $this->add_bindings($join->get_bindings(), 'join');
        } else {
            $method = $where ? 'where' : 'on';
            $this->joins[] = $join->{$method}($first, $operator, $second);
            $this->add_bindings($join->get_bindings(), 'join');
        }
        return $this;
    }
    /**
     * Add a JOIN clause to the query with WHERE condition
     *
     * @param string $table The table to join with
     * @param string $first The first column in the join condition
     * @param string $operator The join operator
     * @param string $second The second column in the join condition
     * @param string $type The type of join (INNER, LEFT, RIGHT)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function join_where($table, $first, $operator, $second, $type = 'inner')
    {
        return $this->join($table, $first, $operator, $second, $type, \true);
    }
    /**
     * Add a JOIN clause to the query with a subquery
     *
     * @param QueryBuilder $query The subquery to join with
     * @param string $as The alias for the subquery
     * @param string $first The first column in the join condition
     * @param string|null $operator The join operator (=, >, <, etc.)
     * @param string|null $second The second column in the join condition
     * @param string $type The type of join (INNER, LEFT, RIGHT)
     * @param bool $where Whether to add a WHERE condition to the join
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function join_sub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = \false)
    {
        [$query, $bindings] = $this->create_subquery($query);
        $expression = \sprintf('(%s) as %s', $query, $this->compiler->wrap_table($as));
        $this->add_bindings($bindings, 'join');
        return $this->join(new Expression($expression), $first, $operator, $second, $type, $where);
    }
    /**
     * Create a new join clause instance
     *
     * @param QueryBuilder $parent_query The parent query builder instance
     * @param string $type The join type (INNER, LEFT, RIGHT)
     * @param string $table The table to join with
     *
     * @return JoinClause
     *
     * @since 1.0.0
     */
    protected function new_join_clause(QueryBuilder $parent_query, $type, $table)
    {
        return new JoinClause($parent_query, $type, $table);
    }
    /**
     * Add a LEFT JOIN clause to the query
     *
     * This method creates a LEFT JOIN that returns all records from the left table and matching
     * records from the right table. If there's no match, NULL values are returned for the right
     * table columns. This is useful when you want to include all records from the primary table
     * regardless of whether they have related records in the joined table.
     *
     * @param string $table The table to join with
     * @param string $first The first column in the join condition
     * @param string|null $operator The join operator (=, >, <, etc.)
     * @param string|null $second The second column in the join condition
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function left_join($table, $first, $operator = null, $second = null)
    {
        return $this->join($table, $first, $operator, $second, 'left');
    }
    /**
     * Add a LEFT JOIN clause to the query with WHERE condition
     *
     * @param string $table The table to join with
     * @param string $first The first column in the join condition
     * @param string $operator The join operator
     * @param string $second The second column in the join condition
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function left_join_where($table, $first, $operator, $second)
    {
        return $this->join_where($table, $first, $operator, $second, 'left');
    }
    /**
     * Add a LEFT JOIN clause to the query with a subquery
     *
     * @param QueryBuilder $query The subquery to join with
     * @param string $as The alias for the subquery
     * @param string $first The first column in the join condition
     * @param string|null $operator The join operator (=, >, <, etc.)
     * @param string|null $second The second column in the join condition
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function left_join_sub($query, $as, $first, $operator = null, $second = null)
    {
        return $this->join_sub($query, $as, $first, $operator, $second, 'left');
    }
    /**
     * Add a RIGHT JOIN clause to the query
     *
     * This method creates a RIGHT JOIN that returns all records from the right table and matching
     * records from the left table. If there's no match, NULL values are returned for the left
     * table columns. This is less commonly used than LEFT JOIN but can be useful in specific
     * scenarios where you want all records from the joined table.
     *
     * @param string $table The table to join with
     * @param string $first The first column in the join condition
     * @param string|null $operator The join operator (=, >, <, etc.)
     * @param string|null $second The second column in the join condition
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function right_join($table, $first, $operator = null, $second = null)
    {
        return $this->join($table, $first, $operator, $second, 'right');
    }
    /**
     * Add a RIGHT JOIN clause to the query with WHERE condition
     *
     * @param string $table The table to join with
     * @param string $first The first column in the join condition
     * @param string $operator The join operator
     * @param string $second The second column in the join condition
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function right_join_where($table, $first, $operator, $second)
    {
        return $this->join_where($table, $first, $operator, $second, 'right');
    }
    /**
     * Add a RIGHT JOIN clause to the query with a subquery
     *
     * @param QueryBuilder $query The subquery to join with
     * @param string $as The alias for the subquery
     * @param string $first The first column in the join condition
     * @param string|null $operator The join operator (=, >, <, etc.)
     * @param string|null $second The second column in the join condition
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function right_join_sub($query, $as, $first, $operator = null, $second = null)
    {
        return $this->join_sub($query, $as, $first, $operator, $second, 'right');
    }
    /**
     * Add GROUP BY clause to group query results by specified columns
     *
     * This method adds columns to the GROUP BY clause, which groups rows that have the same
     * values in the specified columns into summary rows. This is typically used with aggregate
     * functions like COUNT, SUM, AVG, etc. Multiple columns can be specified to create
     * hierarchical grouping.
     *
     * @param mixed $groups The groups.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function group_by(...$groups)
    {
        foreach ($groups as $group) {
            $this->groups = \array_merge((array) $this->groups, Arr::wrap($group));
        }
        return $this;
    }
    /**
     * Add a raw GROUP BY clause to the query
     *
     * This method adds a raw GROUP BY clause to the query. It's useful when you need to
     * group by a column that doesn't exist in the table.
     *
     * @param string $sql The raw SQL query to execute
     * @param array $bindings Parameter values to bind to the query
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function group_by_raw($sql, array $bindings = [])
    {
        $this->groups[] = new Expression($sql);
        $this->add_bindings($bindings, 'group_by');
        return $this;
    }
    /**
     * Add HAVING clause to filter grouped results
     *
     * This method adds a HAVING condition that filters the results after grouping has been
     * applied. Unlike WHERE conditions which filter rows before grouping, HAVING conditions
     * filter groups after they've been formed. This is typically used with aggregate functions
     * to filter based on calculated values.
     *
     * @param string $column The column name to filter on
     * @param string $operator The comparison operator
     * @param mixed $value The value to compare against
     * @param mixed $boolean The boolean.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function having($column, $operator = null, $value = null, $boolean = 'and')
    {
        $type = 'basic';
        if ($column instanceof Expression) {
            $type = 'expression';
            $this->havings[] = \compact('type', 'column', 'boolean');
            return $this;
        }
        [$value, $operator] = $this->prepare_value_and_operator($value, $operator, \func_num_args() === 2);
        if ($column instanceof Closure && \is_null($operator)) {
            return $this->having_nested($column, $boolean);
        }
        if ($this->is_invalid_operator($operator)) {
            [$value, $operator] = [$operator, '='];
        }
        $this->havings[] = \compact('type', 'column', 'value', 'boolean');
        if (!$value instanceof Expression) {
            $this->add_bindings($value, 'having');
        }
        return $this;
    }
    /**
     * Add a HAVING clause to the query with nested condition
     *
     * @param Closure $callback The callback function to add the nested HAVING condition
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    protected function having_nested(Closure $callback, $boolean = 'and')
    {
        $callback($query = $this->for_nested_where());
        return $this->add_nested_having_query($query, $boolean);
    }
    /**
     * Add a HAVING clause to the query with nested condition
     *
     * @param QueryBuilder $query The query builder instance to add the nested HAVING condition to
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    protected function add_nested_having_query($query, $boolean = 'and')
    {
        if (\count($query->havings)) {
            $type = 'nested';
            $this->havings[] = \compact('type', 'query', 'boolean');
            $this->add_bindings($query->get_binding('having'), 'having');
        }
        return $this;
    }
    /**
     * Add a HAVING clause to the query with OR condition
     *
     * This method adds a basic HAVING condition to the query using the OR boolean operator.
     * It functions identically to the having() method but uses OR instead of AND to combine
     * with previous conditions. The operator defaults to '=' if not specified.
     *
     * @param string $column The column name to filter
     * @param string|null $operator The comparison operator (=, >, <, etc.)
     * @param mixed|null $value The value to compare against
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_having($column, $operator = null, $value = null)
    {
        [$value, $operator] = $this->prepare_value_and_operator($value, $operator, \func_num_args() === 2);
        return $this->having($column, $operator, $value, 'or');
    }
    /**
     * Add a HAVING clause to the query with NULL condition
     *
     * @param string $columns The column name to filter
     * @param string $boolean The boolean operator (and, or)
     * @param bool $not Whether the condition should be negated
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function having_null($columns, $boolean = 'and', $not = \false)
    {
        $type = $not ? 'not_null' : 'null';
        foreach (Arr::wrap($columns) as $column) {
            $this->havings[] = \compact('type', 'column', 'boolean');
        }
        return $this;
    }
    /**
     * Add a HAVING clause to the query with NULL condition and OR condition
     *
     * @param string $column The column name to filter
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_having_null($column)
    {
        return $this->having_null($column, 'or');
    }
    /**
     * Add a HAVING clause to the query with NOT NULL condition
     *
     * @param string $columns The column name to filter
     * @param string $boolean The boolean operator (and, or)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function having_not_null($columns, $boolean = 'and')
    {
        return $this->having_null($columns, $boolean, \true);
    }
    /**
     * Add a HAVING clause to the query with NOT NULL condition and OR condition
     *
     * @param string $column The column name to filter
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_having_not_null($column)
    {
        return $this->having_not_null($column, 'or');
    }
    /**
     * Add a HAVING clause to the query with BETWEEN condition
     *
     * @param string $column The column name to filter
     * @param array $values The values to compare against
     * @param string $boolean The boolean operator (and, or)
     * @param bool $not Whether the condition should be negated
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function having_between($column, $values, $boolean = 'and', $not = \false)
    {
        $type = 'between';
        $this->havings[] = \compact('type', 'column', 'values', 'boolean', 'not');
        $this->add_bindings(\array_slice($this->clean_bindings(Arr::flatten($values)), 0, 2), 'having');
        return $this;
    }
    /**
     * Add a HAVING clause to the query with raw SQL condition
     *
     * This method adds a raw HAVING clause to the query. It's useful when you need to
     * group by a column that doesn't exist in the table.
     *
     * @param string $sql The raw SQL query to execute
     * @param array $bindings Parameter values to bind to the query
     * @param string $boolean The boolean operator to combine with previous conditions
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function having_raw($sql, array $bindings = [], $boolean = 'and')
    {
        $type = 'raw';
        $this->havings[] = \compact('type', 'sql', 'boolean');
        $this->add_bindings($bindings, 'having');
        return $this;
    }
    /**
     * Add a HAVING clause to the query with raw SQL condition and OR condition
     *
     * This method adds a raw HAVING clause to the query using the OR boolean operator.
     * It functions identically to the having_raw() method but uses OR instead of AND to combine
     * with previous conditions.
     *
     * @param string $sql The raw SQL query to execute
     * @param array $bindings Parameter values to bind to the query
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function or_having_raw($sql, array $bindings = [])
    {
        return $this->having_raw($sql, $bindings, 'or');
    }
    /**
     * Add a ORDER BY clause to sort query results
     *
     * This method adds a column to sort the query results by. Multiple ORDER BY clauses
     * can be chained to create multi-level sorting. The direction can be either 'ASC'
     * for ascending order or 'DESC' for descending order, with ascending being the default.
     *
     * @param mixed $column The column name to sort by
     * @param string $direction The sort direction (ASC or DESC)
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @throws \InvalidArgumentException
     *
     * @since 1.0.0
     */
    public function order_by($column, $direction = 'asc')
    {
        if ($this->is_queryable($column)) {
            [$query, $bindings] = $this->create_subquery($column);
            $column = new Expression('(' . $query . ')');
            $this->add_bindings($bindings, 'order');
        }
        $direction = \strtolower($direction);
        if (!\in_array($direction, ['asc', 'desc'], \true)) {
            throw new InvalidArgumentException('Order direction must be either "asc" or "desc".');
        }
        $this->orders[] = ['column' => $column, 'direction' => $direction];
        return $this;
    }
    /**
     * Add a ORDER BY clause to the query with DESC direction and latest
     *
     * This method adds a column to sort the query results by in descending order.
     *
     * @param string $column The column name to sort by
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function order_by_desc($column)
    {
        return $this->order_by($column, 'DESC');
    }
    /**
     * Add a ORDER BY clause to the query with DESC direction and oldest
     *
     * This method adds a column to sort the query results by in descending order.
     *
     * @param string $column The column name to sort by
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function latest($column = 'created_at')
    {
        return $this->order_by($column, 'DESC');
    }
    /**
     * Add a ORDER BY clause to the query with ASC direction and oldest
     *
     * This method adds a column to sort the query results by in ascending order.
     *
     * @param string $column The column name to sort by
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function oldest($column = 'created_at')
    {
        return $this->order_by($column, 'ASC');
    }
    /**
     * Add a ORDER BY clause to the query with raw SQL condition
     *
     * @param string $sql The raw SQL query to execute
     * @param array $bindings Parameter values to bind to the query
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function order_by_raw($sql, $bindings = [])
    {
        $type = 'raw';
        $this->orders[] = \compact('type', 'sql');
        $this->add_bindings($bindings, 'order');
        return $this;
    }
    /**
     * Set the maximum number of records to return
     *
     * This method limits the number of records returned by the query. It's commonly used
     * for pagination or when you only need a specific number of results. The limit is
     * applied after all filtering, joining, and sorting operations.
     *
     * @param int $limit The maximum number of records
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function limit($limit)
    {
        if ($limit >= 0) {
            $this->limit = $limit;
        }
        return $this;
    }
    /**
     * Alias for limit() method.
     *
     * @param int $limit The maximum number of records to return
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function take($limit)
    {
        return $this->limit($limit);
    }
    /**
     * Alias for offset() method.
     *
     * @param int $value The number of records to skip
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function skip($value)
    {
        return $this->offset($value);
    }
    /**
     * Set the number of records to skip before returning results
     *
     * This method sets the number of records to skip from the beginning of the result set.
     * It's typically used in conjunction with limit() for pagination purposes. The offset
     * is applied after sorting but before the limit.
     *
     * @param int $offset The number of records to skip
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function offset($offset)
    {
        $this->offset = $offset;
        return $this;
    }
    /**
     * Specify relationships to eager load with the query results
     *
     * This method configures which related models should be loaded along with the main query
     * results to prevent N+1 query problems. Relationships are loaded using the EagerLoader
     * and can significantly improve performance when accessing related data.
     *
     * @param array|string $relations The relationships to load
     * @param mixed $callback The callback to invoke.
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function with($relations, $callback = null)
    {
        if (\is_string($relations)) {
            $relations = \func_get_args();
        }
        if (\is_null($this->with)) {
            $this->with = [];
        }
        if ($callback instanceof Closure) {
            $relations = $this->parse_with_relations([$relations => $callback]);
        } else {
            $relations = $this->parse_with_relations(\is_array($relations) ? $relations : \func_get_args());
        }
        $this->with = \array_merge($this->with, $relations);
        return $this;
    }
    /**
     * Parse relations into a normalized nested structure.
     *
     * @param array $relations The relations to parse
     *
     * @return array The parsed relations
     *
     * @since 1.0.0
     */
    public function parse_with_relations($relations)
    {
        $parsed = [];
        foreach ($relations as $key => $value) {
            if (\is_string($key)) {
                $this->parse_nested_with_relations($key, $parsed, $value);
                continue;
            }
            if (\is_string($value)) {
                $this->parse_nested_with_relations($value, $parsed);
                continue;
            }
            if ($value instanceof Closure) {
                continue;
            }
            if (\is_array($value)) {
                foreach ($value as $nested_key => $nested_value) {
                    \is_numeric($nested_key) ? $this->parse_nested_with_relations($nested_value, $parsed) : $this->parse_nested_with_relations($nested_key, $parsed, $nested_value);
                }
            }
        }
        return $parsed;
    }
    /**
     * Parse a nested relation string or array into the parsed structure.
     *
     * @param string $relation The relation string (may contain dots)
     * @param mixed $parsed The parsed.
     * @param mixed $nested The nested relations if any
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function parse_nested_with_relations($relation, &$parsed, $nested = null)
    {
        if (\strpos($relation, '.') !== \false) {
            $parts = \explode('.', $relation);
            $root = \array_shift($parts);
            $nested_relation = \implode('.', $parts);
            if (!isset($parsed[$root])) {
                $parsed[$root] = [];
            }
            $this->parse_nested_with_relations($nested_relation, $parsed[$root], $nested);
        } else {
            $parsed[$relation] ??= [];
            if (\is_array($nested)) {
                $nested_parsed = $this->parse_with_relations($nested);
                $parsed[$relation] = \array_merge_recursive($parsed[$relation], $nested_parsed);
            } elseif (!empty($nested)) {
                $parsed[$relation][] = $nested;
            }
        }
    }
    /**
     * Specify casts for the query results
     *
     * This method configures which attributes should be cast to a specific type when
     * loading the query results. This is useful for ensuring that attributes are
     * cast to the correct type, such as dates, booleans, or other custom types.
     *
     * @param array $casts The attributes to cast
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function with_casts($casts)
    {
        $this->model->merge_casts($casts);
        return $this;
    }
    /**
     * Call a scope on the query
     *
     * @param callable $scope The scope to call
     * @param array $parameters The parameters to pass to the scope
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function call_scope(callable $scope, array $parameters = [])
    {
        \array_unshift($parameters, $this);
        $query = $this;
        $original_with_count = \is_null($query->wheres) ? 0 : \count($query->wheres);
        $result = $scope(...$parameters) ?? $this;
        if (\count((array) $query->wheres) > $original_with_count) {
            $this->add_new_wheres_within_groups($query, $original_with_count);
        }
        return $result;
    }
    /**
     * Add new wheres from the given query to the current query within a new group
     *
     * @param QueryBuilder $query The query to get new wheres from
     * @param int $original_with_count The original count of wheres in the current query
     *
     * @return void
     *
     * @since 1.0.0
     */
    protected function add_new_wheres_within_groups(QueryBuilder $query, int $original_with_count)
    {
        $all_wheres = $query->wheres;
        $query->wheres = [];
        $this->group_where_slice_for_scope($query, \array_slice($all_wheres, 0, $original_with_count));
        $this->group_where_slice_for_scope($query, \array_slice($all_wheres, $original_with_count));
    }
    /**
     * Group where slice for scope
     *
     * @param QueryBuilder $query The query builder instance to group where slice for
     * @param array $where_slice The where slice to group
     *
     * @return void
     *
     * @since 1.0.0
     */
    protected function group_where_slice_for_scope(QueryBuilder $query, $where_slice)
    {
        $where_booleans = collection($where_slice)->pluck('boolean');
        if ($where_booleans->contains(function ($operator) {
            return str_contains($operator, 'or');
        })) {
            $query->wheres[] = $this->create_nested_where($where_slice, \str_replace(' not', '', $where_booleans->first()));
        } else {
            $query->wheres = \array_merge($query->wheres, $where_slice);
        }
    }
    /**
     * Create a nested where clause
     *
     * @param array $where_slice The where slice to create nested where clause for
     * @param string $boolean The boolean operator (and, or)
     *
     * @return array Returns the nested where clause
     *
     * @since 1.0.0
     */
    protected function create_nested_where($where_slice, $boolean = 'and')
    {
        $where_group = $this->for_nested_where();
        $where_group->wheres = $where_slice;
        return ['type' => 'nested', 'query' => $where_group, 'boolean' => $boolean];
    }
    /**
     * Get the placeholder for a value based on its type
     *
     * @param mixed $value The value to get the placeholder for
     *
     * @return string The placeholder for the value
     *
     * @since 1.0.0
     */
    protected function get_placeholder_by_value($value) : string
    {
        if (\is_null($value)) {
            return 'NULL';
        } elseif (\is_int($value)) {
            return '%d';
        } elseif (\is_float($value)) {
            return '%f';
        }
        return '%s';
    }
    /**
     * Convert the binding values into prepare statement ready placeholders.
     * This is mainly generating %s,%d,%f placeholders for the query.
     *
     * @param mixed $bindings The bindings.
     *
     * @return array
     *
     * @since 1.0.0
     */
    protected function prepare_binding_placeholders($bindings)
    {
        if (!\is_array($bindings) || empty($bindings)) {
            return [];
        }
        $placeholders = [];
        foreach ($bindings as $binding) {
            $placeholders[] = $this->get_placeholder_by_value($binding);
        }
        return $placeholders;
    }
    /**
     * Execute the query and return all matching records as a collection
     *
     * This method executes the built SELECT query and returns all matching records.
     * If a model class is configured, the results are hydrated into model instances.
     * If eager loading relationships are specified, they are loaded to prevent N+1
     * query problems. The final results are wrapped in a Collection instance.
     *
     * @param mixed $columns The columns.
     *
     * @return QueryCollection A collection of model instances or raw data
     *
     * @since 1.0.0
     */
    public function get($columns = ['*'])
    {
        $columns = \is_array($columns) ? $columns : \func_get_args();
        $items = $this->once_with_columns(Arr::wrap($columns), function () {
            return $this->run_select();
        }) ?? [];
        if (!$this->model) {
            return new Collection($items);
        }
        $models = $this->model->hydrate($items);
        return $this->eager_load_relations($models);
    }
    /**
     * Eager load the relationships for the collection.
     *
     * @param Collection $models The models to eager load
     *
     * @return Collection A new collection containing the eager loaded items
     *
     * @since 1.0.0
     */
    public function eager_load_relations(Collection $models)
    {
        if (!empty($this->with) && !empty($models) && static::$should_resolve_relations) {
            $models = Relation::without_constraints(function () use($models) {
                return (new EagerLoader($models, $this->with))->load();
            });
        }
        return $models;
    }
    /**
     * Temporarily disable resolving relations for the duration of the callback.
     *
     * @param Closure $callback The callback to execute with resolving relations disabled
     *
     * @return mixed The return value of the callback
     *
     * @since 1.0.0
     */
    protected static function without_resolving_relations(Closure $callback)
    {
        $previous = static::$should_resolve_relations;
        static::$should_resolve_relations = \false;
        try {
            $result = $callback();
        } finally {
            static::$should_resolve_relations = $previous;
        }
        return $result;
    }
    /**
     * Execute the query and return the results with the specified columns
     *
     * @param array $columns The columns to select
     * @param callable $callback The callback function to execute
     *
     * @return mixed The results of the query
     *
     * @since 1.0.0
     */
    protected function once_with_columns($columns, $callback)
    {
        $original = $this->columns;
        if (\is_null($original)) {
            $this->columns = $columns;
        }
        $result = $callback();
        $this->columns = $original;
        return $result;
    }
    /**
     * Execute the query and return the results
     *
     * @return array The results of the query
     *
     * @since 1.0.0
     */
    protected function run_select()
    {
        return $this->connection->select($this->to_sql(), $this->get_bindings());
    }
    /**
     * Execute the query and return the first matching record
     *
     * This method executes the query with a limit of 1 and returns only the first
     * matching record. If a model class is configured, the result is hydrated into
     * a model instance. If no records match the query conditions, null is returned.
     *
     * @param mixed $columns The columns.
     *
     * @return mixed The first model instance or raw data record, or null if no results
     *
     * @since 1.0.0
     */
    public function first($columns = ['*'])
    {
        return $this->limit(1)->get($columns)->first();
    }
    /**
     * Execute the query and return the first matching record or throw an exception if no record is found.
     *
     * @param array $columns The columns to select
     *
     * @return mixed The first model instance or raw data record
     *
     * @throws ModelNotFoundException
     *
     * @since 1.0.0
     */
    public function first_or_fail($columns = ['*'])
    {
        if (!\is_null($model = $this->first($columns))) {
            return $model;
        }
        throw new ModelNotFoundException(\get_class($this->model));
    }
    /**
     * Pluck the values from the results
     *
     * @param string $column The column to pluck from
     * @param string $key The key to pluck from
     *
     * @return Collection The plucked values
     *
     * @since 1.0.0
     */
    public function pluck($column, $key = null)
    {
        $results = $this->once_with_columns(\is_null($key) ? [$column] : [$column, $key], function () {
            return $this->run_select();
        });
        if (empty($results)) {
            return collection();
        }
        $column = $this->strip_table_for_pluck($column);
        $key = $this->strip_table_for_pluck($key);
        return \is_array($results[0]) ? $this->pluck_from_array($results, $column, $key) : $this->pluck_from_object($results, $column, $key);
    }
    /**
     * Strip the table name from the column name
     *
     * @param string $column The column name
     *
     * @return string The column name without the table name
     *
     * @since 1.0.0
     */
    protected function strip_table_for_pluck($column)
    {
        if (\is_null($column)) {
            return null;
        }
        $column_string = $column instanceof Expression ? $this->compiler->get_value($column) : $column;
        $separator = str_contains(\strtolower($column_string), ' as ') ? ' as ' : '\\.';
        $parts = Str::split($separator, $column_string);
        return array_last($parts);
    }
    /**
     * Pluck the values from the results array
     *
     * @param array $results The results to pluck from
     * @param string $column The column to pluck from
     * @param string $key The key to pluck from
     *
     * @return Collection The plucked values
     *
     * @since 1.0.0
     */
    protected function pluck_from_array($results, $column, $key)
    {
        $data = [];
        if (\is_null($key)) {
            foreach ($results as $result) {
                $data[] = $result[$column];
            }
        } else {
            foreach ($results as $result) {
                $data[$result[$key]] = $result[$column];
            }
        }
        return collection($data);
    }
    /**
     * Pluck the values from the results object
     *
     * @param array $results The results to pluck from
     * @param string $column The column to pluck from
     * @param string $key The key to pluck from
     *
     * @return Collection The plucked values
     *
     * @since 1.0.0
     */
    protected function pluck_from_object($results, $column, $key)
    {
        $data = [];
        if (\is_null($key)) {
            foreach ($results as $result) {
                $data[] = $result->{$column};
            }
        } else {
            foreach ($results as $result) {
                $data[$result->{$key}] = $result->{$column};
            }
        }
        return collection($data);
    }
    /**
     * Find a record by its primary key value
     *
     * This method is a convenience method for finding a single record by its primary key.
     * It adds a WHERE condition for the specified column (defaulting to 'id') and returns
     * the first matching record. This is commonly used for retrieving specific records
     * when you know their unique identifier.
     *
     * @param mixed $id The primary key value to search for
     * @param string $column The column name to search (defaults to 'id')
     *
     * @return Model|null The model instance or raw data record, or null if not found
     *
     * @since 1.0.0
     */
    public function find($id, $column = 'id')
    {
        return $this->where($column, '=', $id)->first();
    }
    /**
     * Count the total number of records that match the query conditions
     *
     * This method executes a COUNT query to determine the total number of records
     * that match the current query conditions. It temporarily modifies the SELECT
     * clause to use COUNT() and then restores the original columns. This is useful
     * for pagination and determining result set sizes without fetching all data.
     *
     * @param string $columns The column to count (defaults to '*' for all records)
     *
     * @return int The total count of matching records
     *
     * @since 1.0.0
     */
    public function count($columns = '*')
    {
        return (int) $this->aggregate(__FUNCTION__, Arr::wrap($columns));
    }
    /**
     * Calculate the sum of values in a specified column
     *
     * This method calculates the sum of all non-null values in the specified column
     * for records that match the current query conditions. It uses the SUM aggregate
     * function and is useful for calculating totals, such as total sales or quantities.
     *
     * @param string $column The column name to sum
     *
     * @return mixed The sum of all values in the column
     *
     * @since 1.0.0
     */
    public function sum($column)
    {
        return $this->aggregate(__FUNCTION__, Arr::wrap($column)) ?: 0;
    }
    /**
     * Calculate the average value of a specified column
     *
     * This method calculates the average (arithmetic mean) of all non-null values
     * in the specified column for records that match the current query conditions.
     * It uses the AVG aggregate function and is useful for statistical analysis
     * and reporting average values.
     *
     * @param string $column The column name to average
     *
     * @return mixed The average value of the column
     *
     * @since 1.0.0
     */
    public function avg($column)
    {
        return $this->aggregate(__FUNCTION__, Arr::wrap($column));
    }
    /**
     * Find the minimum value in a specified column
     *
     * This method finds the smallest value in the specified column for records
     * that match the current query conditions. It uses the MIN aggregate function
     * and can be used with numeric, date, or string columns to find the lowest value.
     *
     * @param string $column The column name to find minimum value
     *
     * @return mixed The minimum value in the column
     *
     * @since 1.0.0
     */
    public function min($column)
    {
        return $this->aggregate(__FUNCTION__, Arr::wrap($column));
    }
    /**
     * Find the maximum value in a specified column
     *
     * This method finds the largest value in the specified column for records
     * that match the current query conditions. It uses the MAX aggregate function
     * and can be used with numeric, date, or string columns to find the highest value.
     *
     * @param string $column The column name to find maximum value
     *
     * @return mixed The maximum value in the column
     *
     * @since 1.0.0
     */
    public function max($column)
    {
        return $this->aggregate(__FUNCTION__, Arr::wrap($column));
    }
    /**
     * Execute an aggregate function on a column and return the result
     *
     * This protected method provides a generic way to execute aggregate functions
     * (SUM, AVG, MIN, MAX) on a specified column. It temporarily modifies the SELECT
     * clause to use the aggregate function, executes the query, and then restores
     * the original column selection.
     *
     * @param string $function The aggregate function to apply (SUM, AVG, MIN, MAX)
     * @param string $columns The column name to apply the function to
     *
     * @return mixed The result of the aggregate function
     *
     * @since 1.0.0
     */
    public function aggregate($function, $columns = ['*'])
    {
        $results = $this->without_resolving_relations(function () use($function, $columns) {
            return $this->clone_without($this->havings ? [] : ['columns'])->clone_without_bindings($this->havings ? [] : ['select'])->set_aggregate($function, $columns)->get();
        });
        if (!$results->empty()) {
            return \array_change_key_case($results->to_array())[0]['aggregate'];
        }
    }
    /**
     * Set the aggregate function and columns for the query
     *
     * @param string $function The aggregate function to apply (SUM, AVG, MIN, MAX)
     * @param string $columns The columns to apply the function to
     *
     * @return QueryBuilder Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    protected function set_aggregate($function, $columns)
    {
        $this->aggregate = \compact('function', 'columns');
        if (empty($this->groups)) {
            $this->orders = null;
            $this->bindings['order'] = [];
        }
        return $this;
    }
    /**
     * Execute an aggregate function on a column and return the result as a numeric value
     *
     * @param string $function The aggregate function to apply (SUM, AVG, MIN, MAX)
     * @param string $columns The columns to apply the function to
     *
     * @return mixed The result of the aggregate function
     *
     * @since 1.0.0
     */
    public function numeric_aggregate($function, $columns = ['*'])
    {
        $result = $this->aggregate($function, $columns);
        if (!$result) {
            return 0;
        }
        if (\is_int($result) || \is_float($result)) {
            return $result;
        }
        return !str_contains((string) $result, '.') ? (int) $result : (float) $result;
    }
    /**
     * Insert a new record into the table with the provided data
     *
     * This method creates an INSERT statement to add a new record to the database.
     * The data array should contain column names as keys and their corresponding
     * values. All values are properly bound to prevent SQL injection attacks.
     * Returns true if the insert was successful, false otherwise.
     *
     * @param array $values Associative array of column names and values to insert
     *
     * @return bool True if the insert was successful, false otherwise
     *
     * @since 1.0.0
     */
    public function insert(array $values)
    {
        if (empty($values)) {
            return \true;
        }
        if (!\is_array(array_first($values))) {
            $values = [$values];
        } else {
            foreach ($values as $key => $value) {
                \ksort($value);
                $values[$key] = $value;
            }
        }
        return $this->connection->insert($this->compiler->compile_insert($this, $values), $this->clean_bindings(Arr::flatten($values, 1)));
    }
    /**
     * Create a new model instance and save it to the database
     *
     * @param array $attributes The attributes to set
     *
     * @return Model The new model instance
     *
     * @since 1.0.0
     */
    public function create(array $attributes = [])
    {
        return tap($this->new_model_instance($attributes), function ($instance) {
            $instance->save();
        });
    }
    /**
     * Update the records that match the query conditions with new data.
     *
     * @param array $values The values to update.
     *
     * @return int The number of rows affected by the update operation.
     *
     * @since 1.0.0
     */
    public function update(array $values)
    {
        $values = (new Collection($values))->map(function ($value) {
            if (!$value instanceof QueryBuilder) {
                return ['value' => $value, 'bindings' => $value instanceof Collection ? $value->all() : $value];
            }
            [$query, $bindings] = $this->parse_subquery($value);
            return ['value' => new Expression(\sprintf('(%s)', $query)), 'bindings' => $bindings];
        });
        $sql = $this->compiler->compile_update($this, $values->map(function ($value) {
            return $value['value'];
        })->all());
        $bindings = $this->clean_bindings($this->compiler->prepare_bindings_for_update($this->bindings, $values->map(function ($value) {
            return $value['bindings'];
        })->all()));
        return $this->connection->update($sql, $bindings);
    }
    /**
     * Update a record or create a new record.
     *
     * @param array $attributes The attributes array.
     * @param array $values The values.
     *
     * @return Model
     *
     * @since 1.0.0
     */
    public function update_or_create(array $attributes, array $values = [])
    {
        return tap($this->first_or_create($attributes, $values), function ($instance) use($values) {
            if (!$instance->was_recently_created) {
                $instance->fill(value($values))->save();
            }
        });
    }
    /**
     * Get the first record or create a new record.
     *
     * @param array $attributes The attributes array.
     * @param mixed $values The values.
     *
     * @return mixed
     *
     * @since 1.0.0
     */
    public function first_or_create(array $attributes, $values = [])
    {
        $instance = (clone $this)->where($attributes)->first();
        if (!\is_null($instance)) {
            return $instance;
        }
        return $this->create_or_first($attributes, $values);
    }
    /**
     * Create a new record or get the first record.
     *
     * @param array $attributes The attributes array.
     * @param mixed $values The values.
     *
     * @return mixed
     *
     * @since 1.0.0
     */
    public function create_or_first(array $attributes, $values = [])
    {
        try {
            return $this->create(\array_merge($attributes, value($values)));
        } catch (UniqueConstraintViolationException $exception) {
            $result = $this->where($attributes)->first();
            if ($result === null) {
                throw $exception;
            }
            return $result;
        }
    }
    /**
     * Upsert the values into the database.
     *
     * @param array $values The values to upsert.
     * @param array $update The columns to update.
     *
     * @return int The number of rows affected by the upsert operation.
     *
     * @since 1.0.0
     */
    public function upsert(array $values, $update = null)
    {
        if (empty($values)) {
            return 0;
        }
        if (!\is_array(array_first($values))) {
            $values = [$values];
        }
        if (\is_null($update)) {
            $update = \array_keys(array_first($values));
        }
        return $this->perform_upsert($this->add_timestamps_to_upsert_values($values), $this->add_updated_at_to_upsert_columns($update));
    }
    /**
     * Add the timestamps to the upsert values.
     *
     * @param array $values The values to add the timestamps to.
     *
     * @return array The values with the timestamps added.
     *
     * @since 1.0.0
     */
    protected function add_timestamps_to_upsert_values(array $values)
    {
        if (!$this->model->uses_timestamps()) {
            return $values;
        }
        $timestamp = $this->model->fresh_timestamp_string();
        $columns = \array_filter([$this->model->get_created_at_column(), $this->model->get_updated_at_column()]);
        foreach ($columns as $column) {
            foreach ($values as &$row) {
                $row = \array_merge([$column => $timestamp], $row);
            }
        }
        return $values;
    }
    /**
     * Add the updated at column to the upsert columns.
     *
     * @param array $update The columns to update.
     *
     * @return array The columns to update.
     *
     * @since 1.0.0
     */
    protected function add_updated_at_to_upsert_columns(array $update)
    {
        if (!$this->model->uses_timestamps()) {
            return $update;
        }
        $column = $this->model->get_updated_at_column();
        if (!\is_null($column) && !\array_key_exists($column, $update) && !\in_array($column, $update)) {
            $update[] = $column;
        }
        return $update;
    }
    /**
     * Perform the upsert operation.
     *
     * @param array $values The values to upsert.
     * @param array $update The columns to update.
     *
     * @return int The number of rows affected by the upsert operation.
     *
     * @since 1.0.0
     */
    protected function perform_upsert(array $values, array $update)
    {
        if ($update === []) {
            return (int) $this->insert($values);
        }
        foreach ($values as $key => $value) {
            \ksort($value);
            $values[$key] = $value;
        }
        $bindings = $this->clean_bindings(\array_merge(Arr::flatten($values, 1), (new Collection($update))->reject(function ($value, $key) {
            return \is_int($key);
        })->all()));
        return $this->connection->affecting_statement($this->compiler->compile_upsert($this, $values, $update), $bindings);
    }
    /**
     * Delete the records that match the query conditions.
     *
     * @return bool|int The number of rows affected by the delete operation, or false on failure.
     *
     * @since 1.0.0
     */
    public function delete()
    {
        return $this->connection->delete($this->compiler->compile_delete($this), $this->clean_bindings($this->compiler->prepare_bindings_for_delete($this->bindings)));
    }
    /**
     * Insert a new record and return the auto-generated primary key
     *
     * This method performs an insert operation and then retrieves the auto-generated
     * primary key value (typically from an AUTO_INCREMENT column). This is useful
     * when you need to know the ID of the newly created record for further operations
     * or to establish relationships with other records.
     *
     * @param array $values Associative array of column names and values to insert
     *
     * @return int The last inserted ID
     *
     * @since 1.0.0
     */
    public function insert_get_id(array $values)
    {
        $result = $this->insert($values);
        if (!$result) {
            return 0;
        }
        // This will return the last insert id after the insert operation.
        // But for multiple inserts, it will return the first attempt id
        // So, use this with caution.
        return (int) $this->connection->get_db()->insert_id;
    }
    /**
     * Increment the value of the given column by the given amount.
     *
     * @param string $column The column to increment.
     * @param int $amount The amount to increment the column by.
     * @param array $extra The extra columns to update.
     *
     * @return bool True if the increment was successful, false otherwise.
     *
     * @throws \InvalidArgumentException
     *
     * @since 1.0.0
     */
    public function increment($column, $amount = 1, array $extra = [])
    {
        if (!\is_numeric($amount)) {
            throw new InvalidArgumentException('Non-numeric value passed to increment method.');
        }
        return $this->increment_each([$column => $amount], $extra);
    }
    /**
     * Increment the values of the given columns by the given amount.
     *
     * @param array $columns The columns to increment.
     * @param array $extra The extra columns to update.
     *
     * @return bool True if the increment was successful, false otherwise.
     *
     * @throws \InvalidArgumentException
     *
     * @since 1.0.0
     */
    public function increment_each(array $columns, array $extra = [])
    {
        foreach ($columns as $column => $amount) {
            if (!\is_numeric($amount)) {
                throw new InvalidArgumentException('Non-numeric value passed to increment method.');
            } elseif (!\is_string($column)) {
                throw new InvalidArgumentException('Invalid column provided to increment method.');
            }
            $columns[$column] = $this->raw(\sprintf('%s + %s', $this->compiler->wrap($column), $amount));
        }
        return $this->update(\array_merge($columns, $extra));
    }
    /**
     * Decrement the value of the given column by the given amount.
     *
     * @param string $column The column to decrement.
     * @param int $amount The amount to decrement the column by.
     * @param array $extra The extra columns to update.
     *
     * @return bool True if the decrement was successful, false otherwise.
     *
     * @throws \InvalidArgumentException
     *
     * @since 1.0.0
     */
    public function decrement($column, $amount = 1, array $extra = [])
    {
        if (!\is_numeric($amount)) {
            throw new InvalidArgumentException('Non-numeric value passed to increment method.');
        }
        return $this->decrement_each([$column => $amount], $extra);
    }
    /**
     * Decrement the values of the given columns by the given amount.
     *
     * @param array $columns The columns to decrement.
     * @param array $extra The extra columns to update.
     *
     * @return bool True if the decrement was successful, false otherwise.
     *
     * @throws \InvalidArgumentException
     *
     * @since 1.0.0
     */
    public function decrement_each(array $columns, array $extra = [])
    {
        foreach ($columns as $column => $amount) {
            if (!\is_numeric($amount)) {
                throw new InvalidArgumentException('Non-numeric value passed to increment method.');
            } elseif (!\is_string($column)) {
                throw new InvalidArgumentException('Invalid column provided to increment method.');
            }
            $columns[$column] = $this->raw(\sprintf('%s - %s', $this->compiler->wrap($column), $amount));
        }
        return $this->update(\array_merge($columns, $extra));
    }
    /**
     * Get the sole record from the query.
     *
     * @param array $columns The columns to select.
     *
     * @return mixed The sole record.
     *
     * @throws RecordNotFoundException
     * @throws MultipleRecordsFoundException
     *
     * @since 1.0.0
     */
    public function sole($columns = ['*'])
    {
        $result = $this->limit(2)->get($columns);
        $count = $result->count();
        if ($count === 0) {
            throw new RecordNotFoundException();
        }
        if ($count > 1) {
            throw new MultipleRecordsFoundException($count);
        }
        return $result->first();
    }
    /**
     * Create a new raw expression.
     *
     * @param string $value The value to create a raw expression from.
     *
     * @return Expression The raw expression.
     *
     * @since 1.0.0
     */
    public function raw($value)
    {
        return $this->connection->raw($value);
    }
    /**
     * Set the page and per_page for the query.
     *
     * @param int $page The page number.
     * @param int $per_page The number of records per page.
     *
     * @return QueryBuilder The query builder instance.
     *
     * @since 1.0.0
     */
    public function for_page($page, $per_page = self::PAGINATION_LIMIT)
    {
        return $this->offset(($page - 1) * $per_page)->limit($per_page);
    }
    /**
     * Get the records for a page before a given ID.
     *
     * @param int $per_page The number of records per page.
     * @param int $last_id The last ID to get records before.
     * @param string $column The column to get records before.
     *
     * @return $this Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function for_page_before_id($per_page = self::PAGINATION_LIMIT, $last_id = 0, $column = 'id')
    {
        $this->orders = $this->remove_existing_orders_for($column);
        if (\is_null($last_id)) {
            $this->where_not_null($column);
        } else {
            $this->where($column, '<', $last_id);
        }
        return $this->order_by($column, 'desc')->limit($per_page);
    }
    /**
     * Get the records for a page after a given ID.
     *
     * @param int $per_page The number of records per page.
     * @param int $last_id The last ID to get records after.
     * @param string $column The column to get records after.
     *
     * @return $this Returns the QueryBuilder instance for method chaining
     *
     * @since 1.0.0
     */
    public function for_page_after_id($per_page = self::PAGINATION_LIMIT, $last_id = 0, $column = 'id')
    {
        $this->orders = $this->remove_existing_orders_for($column);
        if (\is_null($last_id)) {
            $this->where_not_null($column);
        } else {
            $this->where($column, '>', $last_id);
        }
        return $this->order_by($column, 'asc')->limit($per_page);
    }
    /**
     * Remove the existing orders for the given column.
     *
     * @param string $column The column to remove the existing orders for.
     *
     * @return array|null The existing orders without the given column.
     *
     * @since 1.0.0
     */
    protected function remove_existing_orders_for($column)
    {
        if (\is_null($this->orders)) {
            return null;
        }
        return collection($this->orders)->filter(function ($order) use($column) {
            return $order['column'] !== $column;
        })->values()->all();
    }
    /**
     * Enforce the order by primary key.
     *
     * @return void
     *
     * @since 1.0.0
     */
    protected function enforce_order_by_primary_key()
    {
        if (empty($this->orders) && $this->model) {
            $this->order_by($this->model->prepare_column($this->model->get_primary_key()), 'asc');
        }
    }
    /**
     * Reorder the query results.
     *
     * @param string $column The column to reorder by.
     * @param string $direction The direction to reorder by.
     *
     * @return QueryBuilder The query builder instance.
     *
     * @since 1.0.0
     */
    public function reorder($column = null, $direction = 'asc')
    {
        $this->orders = null;
        $this->bindings['order'] = [];
        if ($column) {
            return $this->order_by($column, $direction);
        }
        return $this;
    }
    /**
     * Paginate the query results and return a paginator instance
     *
     * This method implements pagination by counting the total records, calculating
     * the appropriate offset, and limiting the results. It returns a Paginator
     * instance that contains the current page's results along with pagination
     * metadata such as total pages, current page, and navigation information.
     *
     * @param int $per_page Number of records per page
     * @param int $page Current page number
     * @param mixed $columns The columns.
     * @param mixed $total The total.
     *
     * @return Paginator A paginator instance with the results and pagination info
     *
     * @since 1.0.0
     */
    public function paginate($per_page = 15, $page = null, $columns = ['*'], $total = null)
    {
        $columns = Arr::wrap($columns);
        $page = $page ?: 1;
        $total = $total ?? $this->get_count_for_pagination();
        $results = $total ? $this->for_page($page, $per_page)->get($columns) : collection();
        return new Paginator($results, $total, $per_page, $page);
    }
    /**
     * Get the count for pagination.
     *
     * @param array $columns The columns to count.
     *
     * @return int The count for pagination.
     *
     * @since 1.0.0
     */
    protected function get_count_for_pagination($columns = ['*'])
    {
        $results = $this->run_pagination_count_query($columns);
        if (!isset($results[0])) {
            return 0;
        } elseif (\is_object($results[0])) {
            return (int) $results[0]->aggregate;
        }
        return (int) \array_change_key_case((array) $results[0])['aggregate'];
    }
    /**
     * Run the pagination count query.
     *
     * @param array $columns The columns to count.
     *
     * @return array The results of the count query.
     *
     * @since 1.0.0
     */
    protected function run_pagination_count_query($columns = ['*'])
    {
        if ($this->havings || $this->groups) {
            $clone = $this->clone_for_pagination_count();
            if (\is_null($clone->columns) && !empty($this->joins)) {
                $clone->select($this->from . '.*');
            }
            return $this->new_query()->from(new Expression('(' . $clone->to_sql() . ') as ' . $this->compiler->wrap('aggregate_table')))->merge_bindings($clone)->set_aggregate('count', $this->without_select_aliases($columns))->get()->all();
        }
        $without = ['columns', 'orders', 'limit', 'offset'];
        $result = static::without_resolving_relations(function () use($without, $columns) {
            return $this->clone_without($without)->clone_without_bindings(['select', 'order'])->set_aggregate('count', $this->without_select_aliases($columns))->get()->all();
        });
        return $result;
    }
    /**
     * Clone the query for pagination count.
     *
     * @return QueryBuilder The cloned query builder instance.
     *
     * @since 1.0.0
     */
    protected function clone_for_pagination_count()
    {
        return $this->clone_without(['orders', 'limit', 'offset'])->clone_without_bindings(['order']);
    }
    /**
     * Merge the bindings of the query.
     *
     * @param QueryBuilder $query The query to merge the bindings with.
     *
     * @return QueryBuilder The query builder instance.
     *
     * @since 1.0.0
     */
    protected function merge_bindings(self $query)
    {
        $this->bindings = \array_merge_recursive($this->bindings, $query->bindings);
        return $this;
    }
    /**
     * Remove the select aliases from the columns.
     *
     * @param array $columns The columns to remove the aliases from.
     *
     * @return array The columns without the aliases.
     *
     * @since 1.0.0
     */
    protected function without_select_aliases(array $columns)
    {
        return \array_map(function ($column) {
            return \is_string($column) && ($alias_position = \stripos($column, ' as ')) !== \false ? \substr($column, 0, $alias_position) : $column;
        }, $columns);
    }
    /**
     * Get the relationships that are set to be eager loaded
     *
     * This method returns the array of relationship names that have been configured
     * for eager loading through the with() method. This information can be useful
     * for debugging or introspection of the query builder's current state.
     *
     * @return array Array of relationship names to be loaded
     *
     * @since 1.0.0
     */
    public function get_with_relations()
    {
        return $this->with;
    }
    /**
     * Get the database connection instance
     *
     * This method returns the database connection instance that the query builder
     * is using to execute queries. This can be useful for accessing connection-specific
     * methods or for debugging connection-related issues.
     *
     * @return Connection The database connection instance
     *
     * @since 1.0.0
     */
    public function get_connection()
    {
        return $this->connection;
    }
    /**
     * Get the table name being queried
     *
     * This method returns the name of the database table that the query builder
     * is currently configured to query against. This can be useful for debugging
     * or for dynamic query building scenarios.
     *
     * @return string The table name
     *
     * @since 1.0.0
     */
    public function get_table()
    {
        return $this->from;
    }
    /**
     * Get the SQL query string without executing it
     *
     * This method returns the compiled SQL query string that would be executed
     * by the get() or other execution methods. This is useful for debugging,
     * logging, or inspecting the generated SQL before execution. Note that
     * parameter placeholders (?) will still be present in the returned string.
     *
     * @return string The compiled SQL query string
     *
     * @since 1.0.0
     */
    public function to_sql()
    {
        return $this->compiler->compile_select($this);
    }
    /**
     * Get the SQL query string with bindings substituted into it.
     *
     * @return string The SQL query string with bindings substituted into it.
     *
     * @since 1.0.0
     */
    public function to_sql_debug()
    {
        return $this->compiler->substitute_bindings_into_raw_sql($this->to_sql(), $this->connection->prepare_bindings($this->get_bindings()));
    }
    /**
     * Get all parameter bindings for the current query
     *
     * This method returns an array of all parameter values that will be bound
     * to the SQL query when it's executed. The values are in the order they
     * appear in the query and correspond to the parameter placeholders (?).
     * This is useful for debugging and query introspection.
     *
     * @return array Array of parameter values to be bound to the query
     *
     * @since 1.0.0
     */
    public function get_bindings()
    {
        return Arr::flatten($this->bindings);
    }
    /**
     * Get all raw bindings for the current query
     *
     * @return array Array of raw bindings
     *
     * @since 1.0.0
     */
    public function get_raw_bindings()
    {
        return $this->bindings;
    }
    /**
     * Get a binding for a given type
     *
     * @param string $type The type of binding to get
     *
     * @return array Array of bindings
     *
     * @since 1.0.0
     */
    public function get_binding($type)
    {
        return $this->bindings[$type];
    }
    /**
     * Clean the bindings from the query
     *
     * @param array $bindings The bindings to clean
     *
     * @return array Array of cleaned bindings
     *
     * @since 1.0.0
     */
    public function clean_bindings(array $bindings)
    {
        return collection($bindings)->filter(function ($binding) {
            return !$binding instanceof Expression;
        })->values()->all();
    }
    /**
     * Check if the query has a binding for a given type
     *
     * @param string $type The type of binding to check
     *
     * @return bool True if the query has a binding for the given type, false otherwise
     *
     * @since 1.0.0
     */
    protected function bindings_exists(string $type)
    {
        return \array_key_exists($type, $this->bindings);
    }
    /**
     * Check if the query has a binding for a given type
     *
     * @param string $type The type of binding to check
     *
     * @return bool True if the query has a binding for the given type, false otherwise
     *
     * @since 1.0.0
     */
    protected function has_bindings(string $type)
    {
        return $this->bindings_exists($type) && !empty($this->bindings[$type]);
    }
    /**
     * Create a new query builder instance
     *
     * @return QueryBuilder
     *
     * @since 1.0.0
     */
    public function new_query()
    {
        return new static($this->connection, $this->compiler, $this->model);
    }
    /**
     * Create a new model instance
     *
     * @param array $attributes The attributes to set
     *
     * @return Model The new model instance
     *
     * @since 1.0.0
     */
    public function new_model_instance(array $attributes = [])
    {
        return $this->model->new_instance($attributes);
    }
    /**
     * Qualify a column name with the table name
     *
     * @param string|Expression $column The column name to qualify
     *
     * @return string The qualified column name
     *
     * @since 1.0.0
     */
    public function qualify_column($column)
    {
        $column = $column instanceof Expression ? $column->get_value() : $column;
        return $this->model->prepare_column($column);
    }
    /**
     * Create a new query builder instance for a sub-query
     *
     * @return QueryBuilder
     *
     * @since 1.0.0
     */
    protected function for_subquery()
    {
        return $this->new_query();
    }
    /**
     * Check if the value is queryable
     *
     * @param mixed $value The value to check
     *
     * @return bool True if the value is queryable, false otherwise
     *
     * @since 1.0.0
     */
    protected function is_queryable($value)
    {
        return $value instanceof self || $value instanceof Relation || $value instanceof Closure;
    }
    /**
     * Create a subquery from a closure or query builder instance
     *
     * @param Closure|QueryBuilder $query The subquery to create
     *
     * @return array [sql, bindings]
     *
     * @since 1.0.0
     */
    protected function create_subquery($query)
    {
        if ($query instanceof Closure) {
            $callback = $query;
            $callback($query = $this->new_query());
        }
        return $this->parse_subquery($query);
    }
    /**
     * Parse a subquery into a SQL string and bindings
     *
     * @param Closure|QueryBuilder $query The subquery to parse
     *
     * @return array [sql, bindings]
     *
     * @throws \InvalidArgumentException
     *
     * @since 1.0.0
     */
    protected function parse_subquery($query)
    {
        if ($query instanceof self) {
            return [$query->to_sql(), $query->get_bindings()];
        } elseif (\is_string($query)) {
            return [$query, []];
        } else {
            throw new InvalidArgumentException('Invalid subquery provided');
        }
    }
    /**
     * Check if the operator is invalid
     *
     * @param string $operator The operator to check
     *
     * @return bool True if the operator is invalid, false otherwise
     *
     * @since 1.0.0
     */
    protected function is_invalid_operator($operator)
    {
        return !\is_string($operator) || !\in_array(\strtolower($operator), $this->operators, \true);
    }
    /**
     * Check if the operator and value are invalid
     *
     * @param string $operator The operator to check
     * @param mixed $value The value to check
     *
     * @return bool True if the operator and value are invalid, false otherwise
     *
     * @since 1.0.0
     */
    protected function is_invalid_operator_and_value($operator, $value)
    {
        return \is_null($value) && \in_array(\strtolower($operator), $this->operators, \true) && !\in_array(\strtolower($operator), ['=', '<>', '!=']);
    }
    /**
     * Prepare the value and operator
     *
     * @param mixed $value The value to prepare
     * @param string $operator The operator to prepare
     * @param bool $use_default Whether to use the default operator
     *
     * @return array The prepared value and operator
     *
     * @throws \InvalidArgumentException
     *
     * @since 1.0.0
     */
    protected function prepare_value_and_operator($value, $operator, $use_default = \false)
    {
        if ($use_default) {
            return [$operator, '='];
        } elseif ($this->is_invalid_operator_and_value($operator, $value)) {
            throw new InvalidArgumentException('Illegal operator and value combination.');
        }
        return [$value, $operator];
    }
    /**
     * Flatten the value
     *
     * @param mixed $value The value to flatten
     *
     * @return mixed The flattened value
     *
     * @since 1.0.0
     */
    protected function flatten_value($value)
    {
        $flatten = Arr::flatten($value);
        return \is_array($value) ? array_first($flatten) : $value;
    }
    /**
     * Clone the query builder
     *
     * @return QueryBuilder The cloned query builder
     *
     * @since 1.0.0
     */
    public function clone()
    {
        return clone $this;
    }
    /**
     * Clone the query builder without the specified properties
     *
     * @param array $properties The properties to clone without
     *
     * @return QueryBuilder The cloned query builder
     *
     * @since 1.0.0
     */
    protected function clone_without(array $properties)
    {
        $cloned = $this->clone();
        foreach ($properties as $property) {
            $cloned->{$property} = null;
        }
        return $cloned;
    }
    /**
     * Clone the query builder without the specified bindings
     *
     * @param array $except The bindings to clone without
     *
     * @return QueryBuilder The cloned query builder
     *
     * @since 1.0.0
     */
    public function clone_without_bindings(array $except)
    {
        $cloned = $this->clone();
        foreach ($except as $type) {
            $cloned->bindings[$type] = [];
        }
        return $cloned;
    }
    /**
     * Check if the query has a named scope.
     *
     * @param string $scope The scope name to check
     *
     * @return bool True when the scope exists; false otherwise
     *
     * @since 1.0.0
     */
    protected function has_named_scope(string $scope)
    {
        return $this->model && $this->model->has_named_scope($scope);
    }
    /**
     * Call a named scope on the model.
     *
     * @param string $scope The scope name to call
     * @param array $parameters The parameters to pass to the scope
     *
     * @return mixed The result of the scope call
     *
     * @since 1.0.0
     */
    protected function call_named_scope(string $scope, array $parameters = [])
    {
        \array_unshift($parameters, $this);
        return $this->model->call_named_scope($scope, ...$parameters) ?? $this;
    }
    /**
     * Dynamically handle calls to the class
     *
     * @param string $method The method name
     * @param array $parameters The arguments for the method
     *
     * @return mixed The result of the method call
     *
     * @throws \BadMethodCallException
     *
     * @since 1.0.0
     */
    public function __call(string $method, array $parameters)
    {
        if (static::has_macro($method)) {
            return $this->macroCall($method, $parameters);
        }
        if ($this->has_named_scope($method)) {
            return $this->call_named_scope($method, $parameters);
        }
        throw new BadMethodCallException(\sprintf('Method %s::%s does not exist.', QueryBuilder::class, esc_html($method)));
    }
}
