<?php

/**
 * Translates a QueryBuilder state array into parameterized SQL strings.
 * Compiles selects, joins, wheres, aggregates, and pagination clauses for the WordPress database layer.
 * The SQL generation engine behind the fluent query builder.
 *
 * @package    Framework
 * @subpackage Database\Query
 * @since      1.0.0
 */
namespace Kirki\Framework\Database\Query;

\defined('ABSPATH') || exit;
use Kirki\Framework\Collections\Collection;
use Kirki\Framework\Database\Connection\Connection;
use Kirki\Framework\Supports\Arr;
use function Kirki\Framework\collection;
use function Kirki\Framework\Polyfill\array_first;
use function Kirki\Framework\Polyfill\array_last;
class QueryCompiler
{
    /**
     * The database connection instance used to execute queries.
     *
     * @var \Framework\Database\Connection\Connection
     *
     * @since 1.0.0
     */
    protected $connection;
    /**
     * Initialize a new QueryCompiler instance with a database connection.
     *
     * @param Connection $connection The database connection instance.
     *
     * @return void
     *
     * @since 1.0.0
     */
    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }
    /**
     * The components of the query to compile.
     *
     * @var array
     *
     * @since 1.0.0
     */
    protected $components = ['aggregate', 'columns', 'from', 'joins', 'wheres', 'groups', 'havings', 'orders', 'limit', 'offset'];
    /**
     * Get the mysql safe datetime format string.
     *
     * @return string
     *
     * @since 1.0.0
     */
    public function get_date_format()
    {
        return 'Y-m-d H:i:s';
    }
    /**
     * Compile the insert query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $values The values to insert.
     *
     * @return string The compiled insert query.
     *
     * @since 1.0.0
     */
    public function compile_insert(QueryBuilder $query, array $values)
    {
        $table = $this->wrap_table($query->from);
        if (empty($values)) {
            return \sprintf('insert into %s default values', $table);
        }
        if (!\is_array(array_first($values))) {
            $values = [$values];
        }
        $columns = $this->columnize(\array_keys(array_first($values)));
        $parameters = collection($values)->map(function ($record) {
            return \sprintf('(%s)', $this->parameterize($record));
        })->join(', ');
        return \sprintf('insert into %s (%s) values %s', $table, $columns, $parameters);
    }
    /**
     * Compile the update query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $values The values to update.
     *
     * @return string The compiled update query.
     *
     * @since 1.0.0
     */
    public function compile_update(QueryBuilder $query, array $values)
    {
        $table = $this->wrap_table($query->from);
        $columns = $this->compile_update_columns($query, $values);
        $where = $this->compile_wheres($query);
        return \trim(isset($query->joins) ? $this->compile_update_with_joins($query, $table, $columns, $where) : $this->compile_update_without_joins($query, $table, $columns, $where));
    }
    /**
     * Compile the update columns query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $values The values to update.
     *
     * @return string The compiled update columns query.
     *
     * @since 1.0.0
     */
    protected function compile_update_columns(QueryBuilder $query, array $values)
    {
        return collection($values)->map(function ($value, $key) {
            return $this->wrap($key) . ' = ' . $this->parameter($value);
        })->join(', ');
    }
    /**
     * Compile the update with joins query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param string $table The table name.
     * @param string $columns The columns to update.
     * @param string $where The where clause.
     *
     * @return string The compiled update with joins query.
     *
     * @since 1.0.0
     */
    protected function compile_update_with_joins(QueryBuilder $query, $table, $columns, $where)
    {
        $joins = $this->compile_joins($query, $query->joins);
        return \sprintf('update %s %s set %s %s', $table, $joins, $columns, $where);
    }
    /**
     * Compile the update without joins query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param string $table The table name.
     * @param string $columns The columns to update.
     * @param string $where The where clause.
     *
     * @return string The compiled update without joins query.
     *
     * @since 1.0.0
     */
    protected function compile_update_without_joins(QueryBuilder $query, $table, $columns, $where)
    {
        $sql = \sprintf('update %s set %s %s', $table, $columns, $where);
        if (!empty($query->orders)) {
            $sql .= ' ' . $this->compile_orders($query, $query->orders);
        }
        if (!empty($query->limit)) {
            $sql .= ' ' . $this->compile_limit($query, $query->limit);
        }
        return $sql;
    }
    /**
     * Compile the delete query.
     *
     * @param QueryBuilder $query The query builder instance.
     *
     * @return string The compiled delete query.
     *
     * @since 1.0.0
     */
    public function compile_delete(QueryBuilder $query)
    {
        $table = $this->wrap_table($query->from);
        $where = $this->compile_wheres($query);
        return \trim(isset($query->joins) ? $this->compile_delete_with_joins($query, $table, $where) : $this->compile_delete_without_joins($query, $table, $where));
    }
    /**
     * Compile the delete with joins query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param string $table The table name.
     * @param string $where The where clause.
     *
     * @return string The compiled delete with joins query.
     *
     * @since 1.0.0
     */
    protected function compile_delete_with_joins(QueryBuilder $query, $table, $where)
    {
        $parts = \explode(' as ', $table);
        $alias = array_last($parts);
        $joins = $this->compile_joins($query, $query->joins);
        return \sprintf('delete %s from %s %s %s', $alias, $table, $joins, $where);
    }
    /**
     * Compile the delete without joins query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param string $table The table name.
     * @param string $where The where clause.
     *
     * @return string The compiled delete without joins query.
     *
     * @since 1.0.0
     */
    protected function compile_delete_without_joins(QueryBuilder $query, $table, $where)
    {
        return \sprintf('delete from %s %s', $table, $where);
    }
    /**
     * Compile the select query.
     *
     * @param QueryBuilder $query The query builder instance.
     *
     * @return string The compiled select query.
     *
     * @since 1.0.0
     */
    public function compile_select(QueryBuilder $query)
    {
        $original = $query->columns;
        if (\is_null($query->columns)) {
            $query->columns = ['*'];
        }
        $sql = \trim($this->concatenate($this->compile_components($query)));
        $query->columns = $original;
        return $sql;
    }
    /**
     * Compile the components of the query.
     *
     * @param QueryBuilder $query The query builder instance.
     *
     * @return array The compiled components.
     *
     * @since 1.0.0
     */
    protected function compile_components(QueryBuilder $query)
    {
        $sql = [];
        foreach ($this->components as $component) {
            if (isset($query->{$component})) {
                $method = 'compile_' . $component;
                $sql[$component] = $this->{$method}($query, $query->{$component});
            }
        }
        return $sql;
    }
    /**
     * Concatenate the segments.
     *
     * @param array $segments The segments to concatenate.
     *
     * @return string The concatenated segments.
     *
     * @since 1.0.0
     */
    protected function concatenate($segments)
    {
        return \implode(' ', \array_filter($segments, function ($value) {
            return (string) $value !== '';
        }));
    }
    /**
     * Compile the aggregate query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $aggregate The aggregate query.
     *
     * @return string The compiled aggregate query.
     *
     * @since 1.0.0
     */
    protected function compile_aggregate(QueryBuilder $query, $aggregate)
    {
        $column = $this->columnize($aggregate['columns']);
        if (\is_array($query->distinct)) {
            $column = 'distinct ' . $this->columnize($query->distinct);
        } elseif ($query->distinct && $column !== '*') {
            $column = 'distinct ' . $column;
        }
        return \sprintf('select %s(%s) as aggregate', $aggregate['function'], $column);
    }
    /**
     * Compile the columns query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $columns The columns query.
     *
     * @return string|null The compiled columns query.
     *
     * @since 1.0.0
     */
    protected function compile_columns(QueryBuilder $query, $columns)
    {
        if (!\is_null($query->aggregate)) {
            return;
        }
        $select = '';
        if ($query->distinct) {
            $select = 'select distinct ';
        } else {
            $select = 'select ';
        }
        return $select . $this->columnize($columns);
    }
    /**
     * Compile the from query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $table The table query.
     *
     * @return string The compiled from query.
     *
     * @since 1.0.0
     */
    protected function compile_from(QueryBuilder $query, $table)
    {
        return 'from ' . $this->wrap_table($table);
    }
    /**
     * Compile the joins query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $joins The joins query.
     *
     * @return string The compiled joins query.
     *
     * @since 1.0.0
     */
    protected function compile_joins(QueryBuilder $query, $joins)
    {
        return collection($joins)->map(function ($join) use($query) {
            $table = $this->wrap_table($join->table);
            $nested_joins = \is_null($join->joins) ? '' : ' ' . $this->compile_joins($query, $join->joins);
            $table_and_nested_joins = \is_null($join->joins) ? $table : \sprintf('(%s%s)', $table, $nested_joins);
            return \trim(\sprintf('%s join %s %s', $join->type, $table_and_nested_joins, $this->compile_wheres($join)));
        })->join(' ');
    }
    /**
     * Compile the wheres query.
     *
     * @param QueryBuilder $query The query builder instance.
     *
     * @return string The compiled wheres query.
     *
     * @since 1.0.0
     */
    protected function compile_wheres(QueryBuilder $query)
    {
        if (\is_null($query->wheres)) {
            return '';
        }
        $sql = $this->compile_wheres_to_array($query);
        if (\count($sql) > 0) {
            return $this->concatenate_where_clauses($query, $sql);
        }
        return '';
    }
    /**
     * Compile the wheres to array query.
     *
     * @param QueryBuilder $query The query builder instance.
     *
     * @return array The compiled wheres to array query.
     *
     * @since 1.0.0
     */
    protected function compile_wheres_to_array(QueryBuilder $query)
    {
        return \array_map(function ($where) use($query) {
            $boolean = $where['boolean'];
            $method = 'where_' . $where['type'];
            return \sprintf('%s %s', $boolean, $this->{$method}($query, $where));
        }, $query->wheres);
    }
    /**
     * Concatenate the where clauses.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $sql The where clauses.
     *
     * @return string The concatenated where clauses.
     *
     * @since 1.0.0
     */
    protected function concatenate_where_clauses($query, $sql)
    {
        $conjunction = $query instanceof JoinClause ? 'on' : 'where';
        return \sprintf('%s %s', $conjunction, $this->remove_leading_boolean(\implode(' ', $sql)));
    }
    /**
     * Remove the leading boolean.
     *
     * @param string $value The value to remove the leading boolean from.
     *
     * @return string The value with the leading boolean removed.
     *
     * @since 1.0.0
     */
    protected function remove_leading_boolean($value)
    {
        return \preg_replace('/and |or /i', '', $value, 1);
    }
    /**
     * Compile the groups query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $groups The groups query.
     *
     * @return string The compiled groups query.
     *
     * @since 1.0.0
     */
    protected function compile_groups(QueryBuilder $query, $groups)
    {
        return 'group by ' . $this->columnize($groups);
    }
    /**
     * Compile the havings query.
     *
     * @param QueryBuilder $query The query builder instance.
     *
     * @return string The compiled havings query.
     *
     * @since 1.0.0
     */
    protected function compile_havings(QueryBuilder $query)
    {
        $sql = $this->remove_leading_boolean(collection($query->havings)->map(function ($having) {
            return $having['boolean'] . ' ' . $this->compile_having($having);
        })->join(' '));
        return 'having ' . $sql;
    }
    /**
     * Compile the having query.
     *
     * @param array $having The having query.
     *
     * @return string The compiled having query.
     *
     * @since 1.0.0
     */
    protected function compile_having($having)
    {
        switch ($having['type']) {
            case 'raw':
                return $having['sql'];
            case 'between':
                return $this->compile_having_between($having);
            case 'null':
                return $this->compile_having_null($having);
            case 'not_null':
                return $this->compile_having_not_null($having);
            case 'expression':
                return $this->compile_having_expression($having);
            case 'nested':
                return $this->compile_nested_havings($having);
            default:
                return $this->compile_having_basic($having);
        }
    }
    /**
     * Compile the having basic query.
     *
     * @param array $having The having query.
     *
     * @return string The compiled having basic query.
     *
     * @since 1.0.0
     */
    protected function compile_having_basic($having)
    {
        return \sprintf('%s %s %s', $this->wrap($having['column']), $having['operator'], $this->parameter($having['value']));
    }
    /**
     * Compile the nested havings query.
     *
     * @param array $having The having query.
     *
     * @return string The compiled nested havings query.
     *
     * @since 1.0.0
     */
    protected function compile_nested_havings($having)
    {
        return \sprintf('(%s)', \substr($this->compile_havings($having['query']), 7));
    }
    /**
     * Compile the having between query.
     *
     * @param array $having The having query.
     *
     * @return string The compiled having between query.
     *
     * @since 1.0.0
     */
    protected function compile_having_between($having)
    {
        $between = $having['not'] ? 'not between' : 'between';
        $min = $this->parameter($having['values'][0]);
        $max = $this->parameter($having['values'][1]);
        return \sprintf('%s %s %s and %s', $this->wrap($having['column']), $between, $min, $max);
    }
    /**
     * Compile the having null query.
     *
     * @param array $having The having query.
     *
     * @return string The compiled having null query.
     *
     * @since 1.0.0
     */
    protected function compile_having_null($having)
    {
        return $this->wrap($having['column']) . ' is null';
    }
    /**
     * Compile the having not null query.
     *
     * @param array $having The having query.
     *
     * @return string The compiled having not null query.
     *
     * @since 1.0.0
     */
    protected function compile_having_not_null($having)
    {
        return $this->wrap($having['column']) . ' is not null';
    }
    /**
     * Compile the having expression query.
     *
     * @param array $having The having query.
     *
     * @return string The compiled having expression query.
     *
     * @since 1.0.0
     */
    protected function compile_having_expression($having)
    {
        return $having['column']->get_value($this);
    }
    /**
     * Compile the orders query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array|null $orders The orders query.
     *
     * @return string The compiled orders query.
     *
     * @since 1.0.0
     */
    protected function compile_orders(QueryBuilder $query, $orders)
    {
        if (empty($orders)) {
            return '';
        }
        return 'order by ' . \implode(', ', $this->compile_orders_to_array($query, $orders));
    }
    /**
     * Compile the orders to array query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $orders The orders query.
     *
     * @return array The compiled orders to array query.
     *
     * @since 1.0.0
     */
    protected function compile_orders_to_array(QueryBuilder $query, $orders)
    {
        return \array_map(function ($order) {
            return $order['sql'] ?? \sprintf('%s %s', $this->wrap($order['column']), $order['direction']);
        }, $orders);
    }
    /**
     * Compile the limit query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param int $limit The limit query.
     *
     * @return string The compiled limit query.
     *
     * @since 1.0.0
     */
    protected function compile_limit(QueryBuilder $query, $limit)
    {
        return 'limit ' . (int) $limit;
    }
    /**
     * Compile the offset query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $offset The offset query.
     *
     * @return string The compiled offset query.
     *
     * @since 1.0.0
     */
    protected function compile_offset(QueryBuilder $query, $offset)
    {
        return 'offset ' . (int) $offset;
    }
    /**
     * Compile the exists query.
     *
     * @param QueryBuilder $query The query builder instance.
     *
     * @return string The compiled exists query.
     *
     * @since 1.0.0
     */
    public function compile_exists(QueryBuilder $query)
    {
        $select = $this->compile_select($query);
        return \sprintf('select exists(%s) as %s', $select, $this->wrap('exists'));
    }
    /**
     * Compile the truncate query.
     *
     * @param QueryBuilder $query The query builder instance.
     *
     * @return string The compiled truncate query.
     *
     * @since 1.0.0
     */
    public function compile_truncate(QueryBuilder $query)
    {
        return \sprintf('truncate table %s', $this->wrap_table($query->from));
    }
    /**
     * Compile the upsert query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $values The values to upsert.
     * @param array $update The columns to update.
     *
     * @return string The compiled upsert query.
     *
     * @since 1.0.0
     */
    public function compile_upsert(QueryBuilder $query, array $values, array $update)
    {
        $sql = $this->compile_insert($query, $values);
        $sql .= ' on duplicate key update ';
        $columns = (new Collection($update))->map(function ($value, $key) {
            if (!\is_numeric($key)) {
                return $this->wrap($key) . ' = ' . $this->parameter($value);
            }
            return $this->wrap($value) . ' = values(' . $this->wrap($value) . ')';
        })->join(', ');
        return $sql . $columns;
    }
    /**
     * Compile the columnize query.
     *
     * @param array $columns The columns query.
     *
     * @return string The compiled columnize query.
     *
     * @since 1.0.0
     */
    public function columnize(array $columns)
    {
        return \implode(', ', \array_map([$this, 'wrap'], $columns));
    }
    /**
     * Compile the wrap query.
     *
     * @param string|Expression $value The value query.
     *
     * @return string The compiled wrap query.
     *
     * @since 1.0.0
     */
    public function wrap($value)
    {
        if ($this->is_expression($value)) {
            return $this->get_value($value);
        }
        if (\stripos($value, ' as ') !== \false) {
            return $this->wrap_aliased_value($value);
        }
        return $this->wrap_segments(\explode('.', $value));
    }
    /**
     * Compile the is expression query.
     *
     * @param array $value The value query.
     *
     * @return string The compiled is expression query.
     *
     * @since 1.0.0
     */
    public function is_expression($value)
    {
        return $value instanceof Expression;
    }
    /**
     * Compile the get value query.
     *
     * @param string|Expression $expression The expression query.
     *
     * @return string The compiled get value query.
     *
     * @since 1.0.0
     */
    public function get_value($expression)
    {
        if ($this->is_expression($expression)) {
            return $this->get_value($expression->get_value());
        }
        return $expression;
    }
    /**
     * Compile the wrap value query.
     *
     * @param string $value The value query.
     *
     * @return string The compiled wrap value query.
     *
     * @since 1.0.0
     */
    public function wrap_value($value)
    {
        return $value === '*' ? $value : '`' . \str_replace('`', '``', $value) . '`';
    }
    /**
     * Compile the wrap table query.
     *
     * @param string $table The table query.
     * @param string|null $prefix The prefix to use for the table.
     *
     * @return string The compiled wrap table query.
     *
     * @since 1.0.0
     */
    public function wrap_table($table, $prefix = null)
    {
        if ($this->is_expression($table)) {
            return $this->get_value($table);
        }
        $prefix ??= $this->connection->get_table_prefix();
        if (\stripos($table, ' as ') !== \false) {
            return $this->wrap_aliased_table($table, $prefix);
        }
        return $this->wrap_value($this->connection->get_table_prefix() . $table);
    }
    /**
     * Compile the wrap segments query.
     *
     * @param array $segments The segments query.
     *
     * @return string The compiled wrap segments query.
     *
     * @since 1.0.0
     */
    protected function wrap_segments($segments)
    {
        return collection($segments)->map(function ($segment, $key) use($segments) {
            return $key == 0 && \count($segments) > 1 ? $this->wrap_table($segment) : $this->wrap_value($segment);
        })->join('.');
    }
    /**
     * Compile the wrap aliased value query.
     *
     * @param string $value The value query.
     *
     * @return string The compiled wrap aliased value query.
     *
     * @since 1.0.0
     */
    protected function wrap_aliased_value($value)
    {
        $segments = \preg_split('/\\s+as\\s+/i', $value);
        return $this->wrap($segments[0]) . ' as ' . $this->wrap_value($segments[1]);
    }
    /**
     * Compile the wrap aliased table query.
     *
     * @param string $table The table query.
     * @param mixed $prefix The prefix.
     *
     * @return string The compiled wrap aliased table query.
     *
     * @since 1.0.0
     */
    protected function wrap_aliased_table($table, $prefix = null)
    {
        $segments = \preg_split('/\\s+as\\s+/i', $table);
        $prefix ??= $this->connection->get_table_prefix();
        return \sprintf('%s as %s', $this->wrap_table($segments[0], $prefix), $this->wrap_value($prefix . $segments[1]));
    }
    /**
     * Compile the parameter query.
     *
     * @param string|Expression $value The value query.
     *
     * @return string The compiled parameter query.
     *
     * @since 1.0.0
     */
    public function parameter($value)
    {
        return $this->is_expression($value) ? $this->get_value($value) : $this->connection->placeholder($value);
    }
    /**
     * Compile the parameterize query.
     *
     * @param array $values The values query.
     *
     * @return string The compiled parameterize query.
     *
     * @since 1.0.0
     */
    protected function parameterize(array $values)
    {
        return \implode(', ', \array_map([$this, 'parameter'], $values));
    }
    /**
     * Compile the where raw query.
     *
     * @param QueryBuilder $builder The query builder instance.
     * @param array $where The where query.
     *
     * @return string The compiled where raw query.
     *
     * @since 1.0.0
     */
    protected function where_raw(QueryBuilder $builder, $where)
    {
        return $where['sql'] instanceof Expression ? $where['sql']->get_value() : $where['sql'];
    }
    /**
     * Compile the where basic query.
     *
     * @param QueryBuilder $builder The query builder instance.
     * @param array $where The where query.
     *
     * @return string The compiled where basic query.
     *
     * @since 1.0.0
     */
    protected function where_basic(QueryBuilder $builder, $where)
    {
        $value = $this->parameter($where['value']);
        $operator = $where['operator'];
        return \sprintf('%s %s %s', $this->wrap($where['column']), $operator, $value);
    }
    /**
     * Compile the where like query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $where The where query.
     *
     * @return string The compiled where like query.
     *
     * @since 1.0.0
     */
    protected function where_like(QueryBuilder $query, $where)
    {
        $where['operator'] = $where['not'] ? 'not like' : 'like';
        return $this->where_basic($query, $where);
    }
    /**
     * Compile the where in query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param array $where The where query.
     *
     * @return string The compiled where in query.
     *
     * @since 1.0.0
     */
    protected function where_in(QueryBuilder $query, $where)
    {
        if (empty($where['values'])) {
            return '0 = 1';
        }
        return \sprintf('%s in (%s)', $this->wrap($where['column']), $this->parameterize($where['values']));
    }
    /**
     * Compile the where not in query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where not in query.
     *
     * @since 1.0.0
     */
    protected function where_not_in(QueryBuilder $query, $where)
    {
        if (empty($where['values'])) {
            return '1 = 1';
        }
        return \sprintf('%s not in (%s)', $this->wrap($where['column']), $this->parameterize($where['values']));
    }
    /**
     * Compile the where in raw query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where in raw query.
     *
     * @since 1.0.0
     */
    protected function where_in_raw(QueryBuilder $query, $where)
    {
        if (empty($where['values'])) {
            return '0 = 1';
        }
        return \sprintf('%s in (%s)', $this->wrap($where['column']), \implode(', ', $where['values']));
    }
    /**
     * Compile the where not in raw query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where not in raw query.
     *
     * @since 1.0.0
     */
    protected function where_not_in_raw(QueryBuilder $query, $where)
    {
        if (empty($where['values'])) {
            return '1 = 1';
        }
        return \sprintf('%s not in (%s)', $this->wrap($where['column']), \implode(', ', $where['values']));
    }
    /**
     * Compile the where null query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where null query.
     *
     * @since 1.0.0
     */
    protected function where_null(QueryBuilder $query, $where)
    {
        return $this->wrap($where['column']) . ' is null';
    }
    /**
     * Compile the where not null query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where not null query.
     *
     * @since 1.0.0
     */
    protected function where_not_null(QueryBuilder $query, $where)
    {
        return $this->wrap($where['column']) . ' is not null';
    }
    /**
     * Compile the where between query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where between query.
     *
     * @since 1.0.0
     */
    protected function where_between(QueryBuilder $query, $where)
    {
        $between = $where['not'] ? 'not between' : 'between';
        $min = $this->parameter($where['values'][0]);
        $max = $this->parameter($where['values'][1]);
        return \sprintf('%s %s %s and %s', $this->wrap($where['column']), $between, $min, $max);
    }
    /**
     * Compile the where date query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where date query.
     *
     * @since 1.0.0
     */
    protected function where_date(QueryBuilder $query, $where)
    {
        return $this->date_based_where('date', $query, $where);
    }
    /**
     * Compile the where time query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where time query.
     *
     * @since 1.0.0
     */
    protected function where_time(QueryBuilder $query, $where)
    {
        return $this->date_based_where('time', $query, $where);
    }
    /**
     * Compile the date based where query.
     *
     * @param mixed $type The type.
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled date based where query.
     *
     * @since 1.0.0
     */
    protected function date_based_where($type, QueryBuilder $query, $where)
    {
        $value = $this->parameter($where['value']);
        return \sprintf('%s (%s) %s %s', $type, $this->wrap($where['column']), $where['operator'], $value);
    }
    /**
     * Compile the where column query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where column query.
     *
     * @since 1.0.0
     */
    protected function where_column(QueryBuilder $query, $where)
    {
        return \sprintf('%s %s %s', $this->wrap($where['first']), $where['operator'], $this->wrap($where['second']));
    }
    /**
     * Compile the where nested query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where nested query.
     *
     * @since 1.0.0
     */
    protected function where_nested(QueryBuilder $query, $where)
    {
        $offset = $where['query'] instanceof JoinClause ? 3 : 6;
        return \sprintf('(%s)', \substr($this->compile_wheres($where['query']), $offset));
    }
    /**
     * Compile the where subquery query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where subquery query.
     *
     * @since 1.0.0
     */
    protected function where_subquery(QueryBuilder $query, $where)
    {
        $select = $this->compile_select($where['query']);
        return \sprintf('%s %s (%s)', $this->wrap($where['column']), $where['operator'], $select);
    }
    /**
     * Compile the where exists query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where exists query.
     *
     * @since 1.0.0
     */
    protected function where_exists(QueryBuilder $query, $where)
    {
        return \sprintf('exists (%s)', $this->compile_select($where['query']));
    }
    /**
     * Compile the where not exists query.
     *
     * @param QueryBuilder $query The query builder instance.
     * @param mixed $where The where.
     *
     * @return string The compiled where not exists query.
     *
     * @since 1.0.0
     */
    protected function where_not_exists(QueryBuilder $query, $where)
    {
        return \sprintf('not exists (%s)', $this->compile_select($where['query']));
    }
    /**
     * Prepare the bindings for the update query.
     *
     * @param array $bindings The bindings for the query.
     * @param array $values The values to update.
     *
     * @return array The prepared bindings.
     *
     * @since 1.0.0
     */
    public function prepare_bindings_for_update($bindings, $values)
    {
        $clean_bindings = collection($bindings)->filter(function ($value, $key) {
            return !\in_array($key, ['select', 'join'], \true);
        });
        $values = Arr::flatten($values);
        return \array_values(\array_merge($bindings['join'], $values, Arr::flatten($clean_bindings->all())));
    }
    /**
     * Prepare the bindings for the delete query.
     *
     * @param array $bindings The bindings for the query.
     *
     * @return array The prepared bindings.
     *
     * @since 1.0.0
     */
    public function prepare_bindings_for_delete($bindings)
    {
        $clean_bindings = collection($bindings)->filter(function ($value, $key) {
            return !\in_array($key, ['select'], \true);
        });
        return Arr::flatten($clean_bindings->all());
    }
    /**
     * Substitute the bindings into the raw SQL.
     *
     * @param string $sql The raw SQL to substitute the bindings into.
     * @param array $bindings The bindings to substitute into the raw SQL.
     *
     * @return string The substituted raw SQL.
     *
     * @since 1.0.0
     */
    public function substitute_bindings_into_raw_sql($sql, array $bindings = [])
    {
        if (empty($bindings)) {
            return $sql;
        }
        return $this->connection->get_db()->prepare($sql, $bindings);
    }
}
