<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Database;

use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Exceptions\DataException;

/**
 * Class BaseBuilder
 *
 * Provides the core Query Builder methods.
 * Database-specific Builders might need to override
 * certain methods to make them work.
 *
 * @package CodeIgniter\Database
 * @mixin   \CodeIgniter\Model
 */
class BaseBuilder
{

	/**
	 * Reset DELETE data flag
	 *
	 * @var boolean
	 */
	protected $resetDeleteData = false;

	/**
	 * QB SELECT data
	 *
	 * @var array
	 */
	protected $QBSelect = [];

	/**
	 * QB DISTINCT flag
	 *
	 * @var boolean
	 */
	protected $QBDistinct = false;

	/**
	 * QB FROM data
	 *
	 * @var array
	 */
	protected $QBFrom = [];

	/**
	 * QB JOIN data
	 *
	 * @var array
	 */
	protected $QBJoin = [];

	/**
	 * QB WHERE data
	 *
	 * @var array
	 */
	protected $QBWhere = [];

	/**
	 * QB GROUP BY data
	 *
	 * @var array
	 */
	protected $QBGroupBy = [];

	/**
	 * QB HAVING data
	 *
	 * @var array
	 */
	protected $QBHaving = [];

	/**
	 * QB keys
	 *
	 * @var array
	 */
	protected $QBKeys = [];

	/**
	 * QB LIMIT data
	 *
	 * @var integer
	 */
	protected $QBLimit = false;

	/**
	 * QB OFFSET data
	 *
	 * @var integer
	 */
	protected $QBOffset = false;

	/**
	 * QB ORDER BY data
	 *
	 * @var array
	 */
	public $QBOrderBy = [];

	/**
	 * QB NO ESCAPE data
	 *
	 * @var array
	 */
	public $QBNoEscape = [];

	/**
	 * QB data sets
	 *
	 * @var array
	 */
	protected $QBSet = [];

	/**
	 * QB WHERE group started flag
	 *
	 * @var boolean
	 */
	protected $QBWhereGroupStarted = false;

	/**
	 * QB WHERE group count
	 *
	 * @var integer
	 */
	protected $QBWhereGroupCount = 0;

	/**
	 * A reference to the database connection.
	 *
	 * @var BaseConnection
	 */
	protected $db;

	/**
	 * ORDER BY random keyword
	 *
	 * @var array
	 */
	protected $randomKeyword = [
		'RAND()',
		'RAND(%d)',
	];

	/**
	 * COUNT string
	 *
	 * @used-by CI_DB_driver::count_all()
	 * @used-by BaseBuilder::count_all_results()
	 *
	 * @var string
	 */
	protected $countString = 'SELECT COUNT(*) AS ';

	/**
	 * Collects the named parameters and
	 * their values for later binding
	 * in the Query object.
	 *
	 * @var array
	 */
	protected $binds = [];

	/**
	 * Some databases, like SQLite, do not by default
	 * allow limiting of delete clauses.
	 *
	 * @var boolean
	 */
	protected $canLimitDeletes = true;

	/**
	 * Some databases do not by default
	 * allow limit update queries with WHERE.
	 *
	 * @var boolean
	 */
	protected $canLimitWhereUpdates = true;

	//--------------------------------------------------------------------

	/**
	 * Constructor
	 *
	 * @param  string|array                              $tableName
	 * @param  \CodeIgniter\Database\ConnectionInterface $db
	 * @param  array                                     $options
	 * @throws DatabaseException
	 */
	public function __construct($tableName, ConnectionInterface &$db, array $options = null)
	{
		if (empty($tableName))
		{
			throw new DatabaseException('A table must be specified when creating a new Query Builder.');
		}

		$this->db = $db;

		$this->from($tableName);

		if (! empty($options))
		{
			foreach ($options as $key => $value)
			{
				if (property_exists($this, $key))
				{
					$this->$key = $value;
				}
			}
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Returns an array of bind values and their
	 * named parameters for binding in the Query object later.
	 *
	 * @return array
	 */
	public function getBinds(): array
	{
		return $this->binds;
	}

	//--------------------------------------------------------------------

	/**
	 * Select
	 *
	 * Generates the SELECT portion of the query
	 *
	 * @param string|array $select
	 * @param boolean      $escape
	 *
	 * @return BaseBuilder
	 */
	public function select($select = '*', bool $escape = null)
	{
		if (is_string($select))
		{
			$select = explode(',', $select);
		}

		// If the escape value was not set, we will base it on the global setting
		is_bool($escape) || $escape = $this->db->protectIdentifiers;

		foreach ($select as $val)
		{
			$val = trim($val);

			if ($val !== '')
			{
				$this->QBSelect[] = $val;

				/*
				 * When doing 'SELECT NULL as field_alias FROM table'
				 * null gets taken as a field, and therefore escaped
				 * with backticks.
				 * This prevents NULL being escaped
				 * @see https://github.com/codeigniter4/CodeIgniter4/issues/1169
				 */
				if (strtoupper(mb_substr(trim($val), 0, 4)) === 'NULL')
				{
					$escape = false;
				}

				$this->QBNoEscape[] = $escape;
			}
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Select Max
	 *
	 * Generates a SELECT MAX(field) portion of a query
	 *
	 * @param string $select The field
	 * @param string $alias  An alias
	 *
	 * @return BaseBuilder
	 */
	public function selectMax(string $select = '', string $alias = '')
	{
		return $this->maxMinAvgSum($select, $alias, 'MAX');
	}

	//--------------------------------------------------------------------

	/**
	 * Select Min
	 *
	 * Generates a SELECT MIN(field) portion of a query
	 *
	 * @param string $select The field
	 * @param string $alias  An alias
	 *
	 * @return BaseBuilder
	 */
	public function selectMin(string $select = '', string $alias = '')
	{
		return $this->maxMinAvgSum($select, $alias, 'MIN');
	}

	//--------------------------------------------------------------------

	/**
	 * Select Average
	 *
	 * Generates a SELECT AVG(field) portion of a query
	 *
	 * @param string $select The field
	 * @param string $alias  An alias
	 *
	 * @return BaseBuilder
	 */
	public function selectAvg(string $select = '', string $alias = '')
	{
		return $this->maxMinAvgSum($select, $alias, 'AVG');
	}

	//--------------------------------------------------------------------

	/**
	 * Select Sum
	 *
	 * Generates a SELECT SUM(field) portion of a query
	 *
	 * @param string $select The field
	 * @param string $alias  An alias
	 *
	 * @return BaseBuilder
	 */
	public function selectSum(string $select = '', string $alias = '')
	{
		return $this->maxMinAvgSum($select, $alias, 'SUM');
	}

	//--------------------------------------------------------------------

	/**
	 * SELECT [MAX|MIN|AVG|SUM]()
	 *
	 * @used-by selectMax()
	 * @used-by selectMin()
	 * @used-by selectAvg()
	 * @used-by selectSum()
	 *
	 * @param string $select Field name
	 * @param string $alias
	 * @param string $type
	 *
	 * @return BaseBuilder
	 * @throws \CodeIgniter\Database\Exceptions\DataException
	 * @throws \CodeIgniter\Database\Exceptions\DatabaseException
	 */
	protected function maxMinAvgSum(string $select = '', string $alias = '', string $type = 'MAX')
	{
		if ($select === '')
		{
			throw DataException::forEmptyInputGiven('Select');
		}

		if (strpos($select, ',') !== false)
		{
			throw DataException::forInvalidArgument('column name not separated by comma');
		}

		$type = strtoupper($type);

		if (! in_array($type, ['MAX', 'MIN', 'AVG', 'SUM']))
		{
			throw new DatabaseException('Invalid function type: ' . $type);
		}

		if ($alias === '')
		{
			$alias = $this->createAliasFromTable(trim($select));
		}

		$sql = $type . '(' . $this->db->protectIdentifiers(trim($select)) . ') AS ' . $this->db->escapeIdentifiers(trim($alias));

		$this->QBSelect[]   = $sql;
		$this->QBNoEscape[] = null;

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Determines the alias name based on the table
	 *
	 * @param string $item
	 *
	 * @return string
	 */
	protected function createAliasFromTable(string $item): string
	{
		if (strpos($item, '.') !== false)
		{
			$item = explode('.', $item);

			return end($item);
		}

		return $item;
	}

	//--------------------------------------------------------------------

	/**
	 * DISTINCT
	 *
	 * Sets a flag which tells the query string compiler to add DISTINCT
	 *
	 * @param boolean $val
	 *
	 * @return BaseBuilder
	 */
	public function distinct(bool $val = true)
	{
		$this->QBDistinct = $val;

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * From
	 *
	 * Generates the FROM portion of the query
	 *
	 * @param mixed   $from      can be a string or array
	 * @param boolean $overwrite Should we remove the first table existing?
	 *
	 * @return BaseBuilder
	 */
	public function from($from, bool $overwrite = false)
	{
		if ($overwrite === true)
		{
			$this->QBFrom = [];
			$this->db->setAliasedTables([]);
		}

		foreach ((array) $from as $val)
		{
			if (strpos($val, ',') !== false)
			{
				foreach (explode(',', $val) as $v)
				{
					$v = trim($v);
					$this->trackAliases($v);

					$this->QBFrom[] = $v = $this->db->protectIdentifiers($v, true, null, false);
				}
			}
			else
			{
				$val = trim($val);

				// Extract any aliases that might exist. We use this information
				// in the protectIdentifiers to know whether to add a table prefix
				$this->trackAliases($val);

				$this->QBFrom[] = $this->db->protectIdentifiers($val, true, null, false);
			}
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * JOIN
	 *
	 * Generates the JOIN portion of the query
	 *
	 * @param string    $table
	 * @param string    $cond   The join condition
	 * @param string    $type   The type of join
	 * @param boolean   $escape Whether not to try to escape identifiers
	 *
	 * @return BaseBuilder
	 */
	public function join(string $table, string $cond, string $type = '', bool $escape = null)
	{
		if ($type !== '')
		{
			$type = strtoupper(trim($type));

			if (! in_array($type, ['LEFT', 'RIGHT', 'OUTER', 'INNER', 'LEFT OUTER', 'RIGHT OUTER'], true))
			{
				$type = '';
			}
			else
			{
				$type .= ' ';
			}
		}

		// Extract any aliases that might exist. We use this information
		// in the protectIdentifiers to know whether to add a table prefix
		$this->trackAliases($table);

		is_bool($escape) || $escape = $this->db->protectIdentifiers;

		if (! $this->hasOperator($cond))
		{
			$cond = ' USING (' . ($escape ? $this->db->escapeIdentifiers($cond) : $cond) . ')';
		}
		elseif ($escape === false)
		{
			$cond = ' ON ' . $cond;
		}
		else
		{
			// Split multiple conditions
			if (preg_match_all('/\sAND\s|\sOR\s/i', $cond, $joints, PREG_OFFSET_CAPTURE))
			{
				$conditions = [];
				$joints     = $joints[0];
				array_unshift($joints, ['', 0]);

				for ($i = count($joints) - 1, $pos = strlen($cond); $i >= 0; $i --)
				{
					$joints[$i][1] += strlen($joints[$i][0]); // offset
					$conditions[$i] = substr($cond, $joints[$i][1], $pos - $joints[$i][1]);
					$pos            = $joints[$i][1] - strlen($joints[$i][0]);
					$joints[$i]     = $joints[$i][0];
				}
			}
			else
			{
				$conditions = [$cond];
				$joints     = [''];
			}

			$cond = ' ON ';
			for ($i = 0, $c = count($conditions); $i < $c; $i ++)
			{
				$operator = $this->getOperator($conditions[$i]);
				$cond    .= $joints[$i];
				$cond    .= preg_match("/(\(*)?([\[\]\w\.'-]+)" . preg_quote($operator) . '(.*)/i', $conditions[$i], $match) ? $match[1] . $this->db->protectIdentifiers($match[2]) . $operator . $this->db->protectIdentifiers($match[3]) : $conditions[$i];
			}
		}

		// Do we want to escape the table name?
		if ($escape === true)
		{
			$table = $this->db->protectIdentifiers($table, true, null, false);
		}

		// Assemble the JOIN statement
		$this->QBJoin[] = $join = $type . 'JOIN ' . $table . $cond;

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * WHERE
	 *
	 * Generates the WHERE portion of the query.
	 * Separates multiple calls with 'AND'.
	 *
	 * @param mixed   $key
	 * @param mixed   $value
	 * @param boolean $escape
	 *
	 * @return BaseBuilder
	 */
	public function where($key, $value = null, bool $escape = null)
	{
		return $this->whereHaving('QBWhere', $key, $value, 'AND ', $escape);
	}

	//--------------------------------------------------------------------

	/**
	 * OR WHERE
	 *
	 * Generates the WHERE portion of the query.
	 * Separates multiple calls with 'OR'.
	 *
	 * @param mixed   $key
	 * @param mixed   $value
	 * @param boolean $escape
	 *
	 * @return BaseBuilder
	 */
	public function orWhere($key, $value = null, bool $escape = null)
	{
		return $this->whereHaving('QBWhere', $key, $value, 'OR ', $escape);
	}

	//--------------------------------------------------------------------

	/**
	 * WHERE, HAVING
	 *
	 * @used-by where()
	 * @used-by orWhere()
	 * @used-by having()
	 * @used-by orHaving()
	 *
	 * @param string  $qb_key 'QBWhere' or 'QBHaving'
	 * @param mixed   $key
	 * @param mixed   $value
	 * @param string  $type
	 * @param boolean $escape
	 *
	 * @return BaseBuilder
	 */
	protected function whereHaving(string $qb_key, $key, $value = null, string $type = 'AND ', bool $escape = null)
	{
		if (! is_array($key))
		{
			$key = [$key => $value];
		}

		// If the escape value was not set will base it on the global setting
		is_bool($escape) || $escape = $this->db->protectIdentifiers;

		foreach ($key as $k => $v)
		{
			$prefix = empty($this->$qb_key) ? $this->groupGetType('') : $this->groupGetType($type);

			if ($v !== null)
			{
				$op = $this->getOperator($k, true);

				if (! empty($op))
				{
					$k = trim($k);

					end($op);

					$op = trim(current($op));

					if (substr($k, -1 * strlen($op)) === $op)
					{
						$k = rtrim(strrev(preg_replace(strrev('/' . $op . '/'), strrev(''), strrev($k), 1)));
					}
				}

				$bind = $this->setBind($k, $v, $escape);

				if (empty($op))
				{
					$k .= ' =';
				}
				else
				{
					$k .= $op;
				}

				$v = " :$bind:";
			}
			elseif (! $this->hasOperator($k) && $qb_key !== 'QBHaving')
			{
				// value appears not to have been set, assign the test to IS NULL
				$k .= ' IS NULL';
			}
			elseif (preg_match('/\s*(!?=|<>|IS(?:\s+NOT)?)\s*$/i', $k, $match, PREG_OFFSET_CAPTURE))
			{
				$k = substr($k, 0, $match[0][1]) . ($match[1][0] === '=' ? ' IS NULL' : ' IS NOT NULL');
			}

			$this->{$qb_key}[] = [
				'condition' => $prefix . $k . $v,
				'escape'    => $escape,
			];
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * WHERE IN
	 *
	 * Generates a WHERE field IN('item', 'item') SQL query,
	 * joined with 'AND' if appropriate.
	 *
	 * @param string  $key    The field to search
	 * @param array   $values The values searched on
	 * @param boolean $escape
	 *
	 * @return BaseBuilder
	 */
	public function whereIn(string $key = null, array $values = null, bool $escape = null)
	{
		return $this->_whereIn($key, $values, false, 'AND ', $escape);
	}

	//--------------------------------------------------------------------

	/**
	 * OR WHERE IN
	 *
	 * Generates a WHERE field IN('item', 'item') SQL query,
	 * joined with 'OR' if appropriate.
	 *
	 * @param string  $key    The field to search
	 * @param array   $values The values searched on
	 * @param boolean $escape
	 *
	 * @return BaseBuilder
	 */
	public function orWhereIn(string $key = null, array $values = null, bool $escape = null)
	{
		return $this->_whereIn($key, $values, false, 'OR ', $escape);
	}

	//--------------------------------------------------------------------

	/**
	 * WHERE NOT IN
	 *
	 * Generates a WHERE field NOT IN('item', 'item') SQL query,
	 * joined with 'AND' if appropriate.
	 *
	 * @param string  $key    The field to search
	 * @param array   $values The values searched on
	 * @param boolean $escape
	 *
	 * @return BaseBuilder
	 */
	public function whereNotIn(string $key = null, array $values = null, bool $escape = null)
	{
		return $this->_whereIn($key, $values, true, 'AND ', $escape);
	}

	//--------------------------------------------------------------------

	/**
	 * OR WHERE NOT IN
	 *
	 * Generates a WHERE field NOT IN('item', 'item') SQL query,
	 * joined with 'OR' if appropriate.
	 *
	 * @param string  $key    The field to search
	 * @param array   $values The values searched on
	 * @param boolean $escape
	 *
	 * @return BaseBuilder
	 */
	public function orWhereNotIn(string $key = null, array $values = null, bool $escape = null)
	{
		return $this->_whereIn($key, $values, true, 'OR ', $escape);
	}

	//--------------------------------------------------------------------

	/**
	 * Internal WHERE IN
	 *
	 * @used-by WhereIn()
	 * @used-by orWhereIn()
	 * @used-by whereNotIn()
	 * @used-by orWhereNotIn()
	 *
	 * @param string  $key    The field to search
	 * @param array   $values The values searched on
	 * @param boolean $not    If the statement would be IN or NOT IN
	 * @param string  $type
	 * @param boolean $escape
	 *
	 * @return BaseBuilder
	 */
	protected function _whereIn(string $key = null, array $values = null, bool $not = false, string $type = 'AND ', bool $escape = null)
	{
		if ($key === null || $values === null)
		{
			return $this;
		}

		is_bool($escape) || $escape = $this->db->protectIdentifiers;

		$ok = $key;

		if ($escape === true)
		{
			$key = $this->db->protectIdentifiers($key);
		}

		$not = ($not) ? ' NOT' : '';

		$where_in = array_values($values);
		$ok       = $this->setBind($ok, $where_in, $escape);

		$prefix = empty($this->QBWhere) ? $this->groupGetType('') : $this->groupGetType($type);

		$where_in = [
			'condition' => $prefix . $key . $not . " IN :{$ok}:",
			'escape'    => false,
		];

		$this->QBWhere[] = $where_in;

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * LIKE
	 *
	 * Generates a %LIKE% portion of the query.
	 * Separates multiple calls with 'AND'.
	 *
	 * @param mixed   $field
	 * @param string  $match
	 * @param string  $side
	 * @param boolean $escape
	 * @param boolean $insensitiveSearch IF true, will force a case-insensitive search
	 *
	 * @return BaseBuilder
	 */
	public function like($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false)
	{
		return $this->_like($field, $match, 'AND ', $side, '', $escape, $insensitiveSearch);
	}

	//--------------------------------------------------------------------

	/**
	 * NOT LIKE
	 *
	 * Generates a NOT LIKE portion of the query.
	 * Separates multiple calls with 'AND'.
	 *
	 * @param mixed   $field
	 * @param string  $match
	 * @param string  $side
	 * @param boolean $escape
	 * @param boolean $insensitiveSearch IF true, will force a case-insensitive search
	 *
	 * @return BaseBuilder
	 */
	public function notLike($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false)
	{
		return $this->_like($field, $match, 'AND ', $side, 'NOT', $escape, $insensitiveSearch);
	}

	//--------------------------------------------------------------------

	/**
	 * OR LIKE
	 *
	 * Generates a %LIKE% portion of the query.
	 * Separates multiple calls with 'OR'.
	 *
	 * @param mixed   $field
	 * @param string  $match
	 * @param string  $side
	 * @param boolean $escape
	 * @param boolean $insensitiveSearch IF true, will force a case-insensitive search
	 *
	 * @return BaseBuilder
	 */
	public function orLike($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false)
	{
		return $this->_like($field, $match, 'OR ', $side, '', $escape, $insensitiveSearch);
	}

	//--------------------------------------------------------------------

	/**
	 * OR NOT LIKE
	 *
	 * Generates a NOT LIKE portion of the query.
	 * Separates multiple calls with 'OR'.
	 *
	 * @param mixed   $field
	 * @param string  $match
	 * @param string  $side
	 * @param boolean $escape
	 * @param boolean $insensitiveSearch IF true, will force a case-insensitive search
	 *
	 * @return BaseBuilder
	 */
	public function orNotLike($field, string $match = '', string $side = 'both', bool $escape = null, bool $insensitiveSearch = false)
	{
		return $this->_like($field, $match, 'OR ', $side, 'NOT', $escape, $insensitiveSearch);
	}

	//--------------------------------------------------------------------

	/**
	 * Internal LIKE
	 *
	 * @used-by like()
	 * @used-by orLike()
	 * @used-by notLike()
	 * @used-by orNotLike()
	 *
	 * @param mixed   $field
	 * @param string  $match
	 * @param string  $type
	 * @param string  $side
	 * @param string  $not
	 * @param boolean $escape
	 * @param boolean $insensitiveSearch IF true, will force a case-insensitive search
	 *
	 * @return BaseBuilder
	 */
	protected function _like($field, string $match = '', string $type = 'AND ', string $side = 'both', string $not = '', bool $escape = null, bool $insensitiveSearch = false)
	{
		if (! is_array($field))
		{
			$field = [$field => $match];
		}

		$escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers;

		// lowercase $side in case somebody writes e.g. 'BEFORE' instead of 'before' (doh)
		$side = strtolower($side);

		foreach ($field as $k => $v)
		{
			$prefix = empty($this->QBWhere) ? $this->groupGetType('') : $this->groupGetType($type);

			if ($insensitiveSearch === true)
			{
				$v = strtolower($v);
			}

			if ($side === 'none')
			{
				$bind = $this->setBind($k, $v, $escape);
			}
			elseif ($side === 'before')
			{
				$bind = $this->setBind($k, "%$v", $escape);
			}
			elseif ($side === 'after')
			{
				$bind = $this->setBind($k, "$v%", $escape);
			}
			else
			{
				$bind = $this->setBind($k, "%$v%", $escape);
			}

			$like_statement = $this->_like_statement($prefix, $k, $not, $bind, $insensitiveSearch);

			// some platforms require an escape sequence definition for LIKE wildcards
			if ($escape === true && $this->db->likeEscapeStr !== '')
			{
				$like_statement .= sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar);
			}

			$this->QBWhere[] = [
				'condition' => $like_statement,
				'escape'    => $escape,
			];
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Platform independent LIKE statement builder.
	 *
	 * @param string    $prefix
	 * @param string    $column
	 * @param string    $not
	 * @param string    $bind
	 * @param boolean   $insensitiveSearch
	 *
	 * @return string     $like_statement
	 */
	protected function _like_statement(string $prefix = null, string $column, string $not = null, string $bind, bool $insensitiveSearch = false): string
	{
		$like_statement = "{$prefix} {$column} {$not} LIKE :{$bind}:";

		if ($insensitiveSearch === true)
		{
			$like_statement = "{$prefix} LOWER({$column}) {$not} LIKE :{$bind}:";
		}

		return $like_statement;
	}

	//--------------------------------------------------------------------

	/**
	 * Starts a query group.
	 *
	 * @param string $not  (Internal use only)
	 * @param string $type (Internal use only)
	 *
	 * @return BaseBuilder
	 */
	public function groupStart(string $not = '', string $type = 'AND ')
	{
		$type = $this->groupGetType($type);

		$this->QBWhereGroupStarted = true;
		$prefix                    = empty($this->QBWhere) ? '' : $type;
		$where                     = [
			'condition' => $prefix . $not . str_repeat(' ', ++ $this->QBWhereGroupCount) . ' (',
			'escape'    => false,
		];

		$this->QBWhere[] = $where;

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Starts a query group, but ORs the group
	 *
	 * @return BaseBuilder
	 */
	public function orGroupStart()
	{
		return $this->groupStart('', 'OR ');
	}

	//--------------------------------------------------------------------

	/**
	 * Starts a query group, but NOTs the group
	 *
	 * @return BaseBuilder
	 */
	public function notGroupStart()
	{
		return $this->groupStart('NOT ', 'AND ');
	}

	//--------------------------------------------------------------------

	/**
	 * Starts a query group, but OR NOTs the group
	 *
	 * @return BaseBuilder
	 */
	public function orNotGroupStart()
	{
		return $this->groupStart('NOT ', 'OR ');
	}

	//--------------------------------------------------------------------

	/**
	 * Ends a query group
	 *
	 * @return BaseBuilder
	 */
	public function groupEnd()
	{
		$this->QBWhereGroupStarted = false;
		$where                     = [
			'condition' => str_repeat(' ', $this->QBWhereGroupCount -- ) . ')',
			'escape'    => false,
		];

		$this->QBWhere[] = $where;

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Group_get_type
	 *
	 * @used-by groupStart()
	 * @used-by _like()
	 * @used-by whereHaving()
	 * @used-by _whereIn()
	 *
	 * @param string $type
	 *
	 * @return string
	 */
	protected function groupGetType(string $type): string
	{
		if ($this->QBWhereGroupStarted)
		{
			$type = '';
			$this->QBWhereGroupStarted = false;
		}

		return $type;
	}

	//--------------------------------------------------------------------

	/**
	 * GROUP BY
	 *
	 * @param string|array  $by
	 * @param boolean       $escape
	 *
	 * @return BaseBuilder
	 */
	public function groupBy($by, bool $escape = null)
	{
		is_bool($escape) || $escape = $this->db->protectIdentifiers;

		if (is_string($by))
		{
			$by = ($escape === true) ? explode(',', $by) : [$by];
		}

		foreach ($by as $val)
		{
			$val = trim($val);

			if ($val !== '')
			{
				$val = [
					'field'  => $val,
					'escape' => $escape,
				];

				$this->QBGroupBy[] = $val;
			}
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * HAVING
	 *
	 * Separates multiple calls with 'AND'.
	 *
	 * @param string|array  $key
	 * @param mixed         $value
	 * @param boolean       $escape
	 *
	 * @return BaseBuilder
	 */
	public function having($key, $value = null, bool $escape = null)
	{
		return $this->whereHaving('QBHaving', $key, $value, 'AND ', $escape);
	}

	//--------------------------------------------------------------------

	/**
	 * OR HAVING
	 *
	 * Separates multiple calls with 'OR'.
	 *
	 * @param string|array  $key
	 * @param mixed         $value
	 * @param boolean       $escape
	 *
	 * @return BaseBuilder
	 */
	public function orHaving($key, $value = null, bool $escape = null)
	{
		return $this->whereHaving('QBHaving', $key, $value, 'OR ', $escape);
	}

	//--------------------------------------------------------------------

	/**
	 * ORDER BY
	 *
	 * @param string  $orderBy
	 * @param string  $direction ASC, DESC or RANDOM
	 * @param boolean $escape
	 *
	 * @return BaseBuilder
	 */
	public function orderBy(string $orderBy, string $direction = '', bool $escape = null)
	{
		$direction = strtoupper(trim($direction));

		if ($direction === 'RANDOM')
		{
			$direction = '';

			// Do we have a seed value?
			$orderBy = ctype_digit((string) $orderBy) ? sprintf($this->randomKeyword[1], $orderBy) : $this->randomKeyword[0];
		}
		elseif (empty($orderBy))
		{
			return $this;
		}
		elseif ($direction !== '')
		{
			$direction = in_array($direction, ['ASC', 'DESC'], true) ? ' ' . $direction : '';
		}

		is_bool($escape) || $escape = $this->db->protectIdentifiers;

		if ($escape === false)
		{
			$qb_orderBy[] = [
				'field'     => $orderBy,
				'direction' => $direction,
				'escape'    => false,
			];
		}
		else
		{
			$qb_orderBy = [];
			foreach (explode(',', $orderBy) as $field)
			{
				$qb_orderBy[] = ($direction === '' && preg_match('/\s+(ASC|DESC)$/i', rtrim($field), $match, PREG_OFFSET_CAPTURE))
					?
					[
						'field'     => ltrim(substr($field, 0, $match[0][1])),
						'direction' => ' ' . $match[1][0],
						'escape'    => true,
					]
					:
					[
						'field'     => trim($field),
						'direction' => $direction,
						'escape'    => true,
					];
			}
		}

		$this->QBOrderBy = array_merge($this->QBOrderBy, $qb_orderBy);

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * LIMIT
	 *
	 * @param integer $value  LIMIT value
	 * @param integer $offset OFFSET value
	 *
	 * @return BaseBuilder
	 */
	public function limit(int $value = null, int $offset = 0)
	{
		if (! is_null($value))
		{
			$this->QBLimit = $value;
		}

		if (! empty($offset))
		{
			$this->QBOffset = $offset;
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Sets the OFFSET value
	 *
	 * @param integer $offset OFFSET value
	 *
	 * @return BaseBuilder
	 */
	public function offset(int $offset)
	{
		if (! empty($offset))
		{
			$this->QBOffset = (int) $offset;
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * LIMIT string
	 *
	 * Generates a platform-specific LIMIT clause.
	 *
	 * @param string $sql SQL Query
	 *
	 * @return string
	 */
	protected function _limit(string $sql): string
	{
		return $sql . ' LIMIT ' . ($this->QBOffset ? $this->QBOffset . ', ' : '') . $this->QBLimit;
	}

	//--------------------------------------------------------------------

	/**
	 * The "set" function.
	 *
	 * Allows key/value pairs to be set for insert(), update() or replace().
	 *
	 * @param string|array|object $key    Field name, or an array of field/value pairs
	 * @param string              $value  Field value, if $key is a single field
	 * @param boolean             $escape Whether to escape values and identifiers
	 *
	 * @return BaseBuilder
	 */
	public function set($key, string $value = '', bool $escape = null)
	{
		$key = $this->objectToArray($key);

		if (! is_array($key))
		{
			$key = [$key => $value];
		}

		$escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers;

		foreach ($key as $k => $v)
		{
			if ($escape)
			{
				$bind = $this->setBind($k, $v, $escape);
				$this->QBSet[$this->db->protectIdentifiers($k, false, $escape)] = ":$bind:";
			}
			else
			{
				$this->QBSet[$this->db->protectIdentifiers($k, false, $escape)] = $v;
			}
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns the previously set() data, alternatively resetting it
	 * if needed.
	 *
	 * @param boolean $clean
	 *
	 * @return array
	 */
	public function getSetData(bool $clean = false): array
	{
		$data = $this->QBSet;

		if ($clean)
		{
			$this->QBSet = [];
		}

		return $data;
	}

	//--------------------------------------------------------------------

	/**
	 * Get SELECT query string
	 *
	 * Compiles a SELECT query string and returns the sql.
	 *
	 * @param boolean $reset TRUE: resets QB values; FALSE: leave QB values alone
	 *
	 * @return string
	 */
	public function getCompiledSelect(bool $reset = true): string
	{
		$select = $this->compileSelect();

		if ($reset === true)
		{
			$this->resetSelect();
		}

		return $this->compileFinalQuery($select);
	}

	//--------------------------------------------------------------------

	/**
	 * Returns a finalized, compiled query string with the bindings
	 * inserted and prefixes swapped out.
	 *
	 * @param string $sql
	 *
	 * @return string
	 */
	protected function compileFinalQuery(string $sql): string
	{
		$query = new Query($this->db);
		$query->setQuery($sql, $this->binds, false);

		if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix))
		{
			$query->swapPrefix($this->db->DBPrefix, $this->db->swapPre);
		}

		return $query->getQuery();
	}

	/**
	 * Get
	 *
	 * Compiles the select statement based on the other functions called
	 * and runs the query
	 *
	 * @param integer $limit     The limit clause
	 * @param integer $offset    The offset clause
	 * @param boolean $returnSQL If true, returns the generate SQL, otherwise executes the query.
	 * @param boolean $reset     Are we want to clear query builder values?
	 *
	 * @return ResultInterface
	 */
	public function get(int $limit = null, int $offset = 0, bool $returnSQL = false, bool $reset = true)
	{
		if (! is_null($limit))
		{
			$this->limit($limit, $offset);
		}

		$result = $returnSQL
			? $this->getCompiledSelect()
			: $this->db->query($this->compileSelect(), $this->binds, false);

		if ($reset === true)
		{
			$this->resetSelect();

			// Clear our binds so we don't eat up memory
			$this->binds = [];
		}

		return $result;
	}

	//--------------------------------------------------------------------

	/**
	 * "Count All" query
	 *
	 * Generates a platform-specific query string that counts all records in
	 * the specified database
	 *
	 * @param boolean $reset Are we want to clear query builder values?
	 * @param boolean $test  Are we running automated tests?
	 *
	 * @return integer|string when $test = true
	 */
	public function countAll(bool $reset = true, bool $test = false)
	{
		$table = $this->QBFrom[0];

		$sql = $this->countString . $this->db->escapeIdentifiers('numrows') . ' FROM ' .
				$this->db->protectIdentifiers($table, true, null, false);

		if ($test)
		{
			return $sql;
		}

		$query = $this->db->query($sql, null, false);
		if (empty($query->getResult()))
		{
			return 0;
		}

		$query = $query->getRow();

		if ($reset === true)
		{
			$this->resetSelect();
		}

		return (int) $query->numrows;
	}

	//--------------------------------------------------------------------

	/**
	 * "Count All Results" query
	 *
	 * Generates a platform-specific query string that counts all records
	 * returned by an Query Builder query.
	 *
	 * @param boolean $reset
	 * @param boolean $test  The reset clause
	 *
	 * @return integer|string when $test = true
	 */
	public function countAllResults(bool $reset = true, bool $test = false)
	{
		// ORDER BY usage is often problematic here (most notably
		// on Microsoft SQL Server) and ultimately unnecessary
		// for selecting COUNT(*) ...
		$orderBy = [];
		if (! empty($this->QBOrderBy))
		{
			$orderBy         = $this->QBOrderBy;
			$this->QBOrderBy = null;
		}

		$sql = ($this->QBDistinct === true)
			?
			$this->countString . $this->db->protectIdentifiers('numrows') . "\nFROM (\n" . $this->compileSelect() . "\n) CI_count_all_results"
			:
			$this->compileSelect($this->countString . $this->db->protectIdentifiers('numrows'));

		if ($test)
		{
			return $sql;
		}

		$result = $this->db->query($sql, $this->binds, false);

		if ($reset === true)
		{
			$this->resetSelect();
		}
		// If we've previously reset the QBOrderBy values, get them back
		elseif (! isset($this->QBOrderBy))
		{
			$this->QBOrderBy = $orderBy ?? [];
		}

		$row = (! $result instanceof ResultInterface)
			? null
			: $result->getRow();

		if (empty($row))
		{
			return 0;
		}

		return (int) $row->numrows;
	}

	//--------------------------------------------------------------------

	/**
	 * Get_Where
	 *
	 * Allows the where clause, limit and offset to be added directly
	 *
	 * @param string|array  $where
	 * @param integer       $limit
	 * @param integer       $offset
	 *
	 * @return ResultInterface
	 */
	public function getWhere($where = null, int $limit = null, int $offset = null)
	{
		if ($where !== null)
		{
			$this->where($where);
		}

		if (! empty($limit))
		{
			$this->limit($limit, $offset);
		}

		$result = $this->db->query($this->compileSelect(), $this->binds, false);
		$this->resetSelect();

		return $result;
	}

	//--------------------------------------------------------------------

	/**
	 * Insert_Batch
	 *
	 * Compiles batch insert strings and runs the queries
	 *
	 * @param array   $set       An associative array of insert values
	 * @param boolean $escape    Whether to escape values and identifiers
	 *
	 * @param integer $batchSize
	 * @param boolean $testing
	 *
	 * @return integer Number of rows inserted or FALSE on failure
	 * @throws DatabaseException
	 */
	public function insertBatch(array $set = null, bool $escape = null, int $batchSize = 100, bool $testing = false)
	{
		if ($set === null)
		{
			if (empty($this->QBSet))
			{
				if (CI_DEBUG)
				{
					throw new DatabaseException('You must use the "set" method to update an entry.');
				}

				return false;
			}
		}
		else
		{
			if (empty($set))
			{
				if (CI_DEBUG)
				{
					throw new DatabaseException('insertBatch() called with no data');
				}

				return false;
			}

			$this->setInsertBatch($set, '', $escape);
		}

		$table = $this->QBFrom[0];

		// Batch this baby
		$affected_rows = 0;
		for ($i = 0, $total = count($this->QBSet); $i < $total; $i += $batchSize)
		{
			$sql = $this->_insertBatch($this->db->protectIdentifiers($table, true, $escape, false), $this->QBKeys, array_slice($this->QBSet, $i, $batchSize));

			if ($testing)
			{
				++ $affected_rows;
			}
			else
			{
				$this->db->query($sql, $this->binds, false);
				$affected_rows += $this->db->affectedRows();
			}
		}

		if (! $testing)
		{
			$this->resetWrite();
		}

		return $affected_rows;
	}

	//--------------------------------------------------------------------

	/**
	 * Insert batch statement
	 *
	 * Generates a platform-specific insert string from the supplied data.
	 *
	 * @param string $table  Table name
	 * @param array  $keys   INSERT keys
	 * @param array  $values INSERT values
	 *
	 * @return string
	 */
	protected function _insertBatch(string $table, array $keys, array $values): string
	{
		return 'INSERT INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES ' . implode(', ', $values);
	}

	//--------------------------------------------------------------------

	/**
	 * The "setInsertBatch" function.  Allows key/value pairs to be set for batch inserts
	 *
	 * @param mixed   $key
	 * @param string  $value
	 * @param boolean $escape
	 *
	 * @return BaseBuilder|null
	 */
	public function setInsertBatch($key, string $value = '', bool $escape = null)
	{
		$key = $this->batchObjectToArray($key);

		if (! is_array($key))
		{
			$key = [$key => $value];
		}

		$escape = is_bool($escape) ? $escape : $this->db->protectIdentifiers;

		$keys = array_keys($this->objectToArray(current($key)));
		sort($keys);

		foreach ($key as $row)
		{
			$row = $this->objectToArray($row);
			if (count(array_diff($keys, array_keys($row))) > 0 || count(array_diff(array_keys($row), $keys)) > 0)
			{
				// batch function above returns an error on an empty array
				$this->QBSet[] = [];

				return null;
			}

			ksort($row); // puts $row in the same order as our keys

			$clean = [];
			foreach ($row as $k => $value)
			{
				$clean[] = ':' . $this->setBind($k, $value, $escape) . ':';
			}

			$row = $clean;

			$this->QBSet[] = '(' . implode(',', $row) . ')';
		}

		foreach ($keys as $k)
		{
			$this->QBKeys[] = $this->db->protectIdentifiers($k, false, $escape);
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Get INSERT query string
	 *
	 * Compiles an insert query and returns the sql
	 *
	 * @param boolean $reset TRUE: reset QB values; FALSE: leave QB values alone
	 *
	 * @return string
	 */
	public function getCompiledInsert(bool $reset = true): string
	{
		if ($this->validateInsert() === false)
		{
			return false;
		}

		$sql = $this->_insert(
				$this->db->protectIdentifiers(
						$this->QBFrom[0], true, null, false
				), array_keys($this->QBSet), array_values($this->QBSet)
		);

		if ($reset === true)
		{
			$this->resetWrite();
		}

		return $this->compileFinalQuery($sql);
	}

	//--------------------------------------------------------------------

	/**
	 * Insert
	 *
	 * Compiles an insert string and runs the query
	 *
	 * @param array   $set    An associative array of insert values
	 * @param boolean $escape Whether to escape values and identifiers
	 * @param boolean $test   Used when running tests
	 *
	 * @return BaseResult|Query|false
	 */
	public function insert(array $set = null, bool $escape = null, bool $test = false)
	{
		if ($set !== null)
		{
			$this->set($set, '', $escape);
		}

		if ($this->validateInsert() === false)
		{
			return false;
		}

		$sql = $this->_insert(
				$this->db->protectIdentifiers(
						$this->QBFrom[0], true, $escape, false
				), array_keys($this->QBSet), array_values($this->QBSet)
		);

		if ($test === false)
		{
			$this->resetWrite();

			$result = $this->db->query($sql, $this->binds, false);

			// Clear our binds so we don't eat up memory
			$this->binds = [];

			return $result;
		}

		return false;
	}

	//--------------------------------------------------------------------

	/**
	 * Validate Insert
	 *
	 * This method is used by both insert() and getCompiledInsert() to
	 * validate that the there data is actually being set and that table
	 * has been chosen to be inserted into.
	 *
	 * @return bool
	 * @throws DatabaseException
	 */
	protected function validateInsert(): bool
	{
		if (empty($this->QBSet))
		{
			if (CI_DEBUG)
			{
				throw new DatabaseException('You must use the "set" method to update an entry.');
			}

			return false;
		}

		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Insert statement
	 *
	 * Generates a platform-specific insert string from the supplied data
	 *
	 * @param string $table         The table name
	 * @param array  $keys          The insert keys
	 * @param array  $unescapedKeys The insert values
	 *
	 * @return string
	 */
	protected function _insert(string $table, array $keys, array $unescapedKeys): string
	{
		return 'INSERT INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $unescapedKeys) . ')';
	}

	//--------------------------------------------------------------------

	/**
	 * Replace
	 *
	 * Compiles an replace into string and runs the query
	 *
	 * @param array   $set       An associative array of insert values
	 * @param boolean $returnSQL
	 *
	 * @return BaseResult|Query|string|false
	 * @throws DatabaseException
	 */
	public function replace(array $set = null, bool $returnSQL = false)
	{
		if ($set !== null)
		{
			$this->set($set);
		}

		if (empty($this->QBSet))
		{
			if (CI_DEBUG)
			{
				throw new DatabaseException('You must use the "set" method to update an entry.');
			}
			return false;
		}

		$table = $this->QBFrom[0];

		$sql = $this->_replace($table, array_keys($this->QBSet), array_values($this->QBSet));

		$this->resetWrite();

		return $returnSQL ? $sql : $this->db->query($sql, $this->binds, false);
	}

	//--------------------------------------------------------------------

	/**
	 * Replace statement
	 *
	 * Generates a platform-specific replace string from the supplied data
	 *
	 * @param string $table  The table name
	 * @param array  $keys   The insert keys
	 * @param array  $values The insert values
	 *
	 * @return string
	 */
	protected function _replace(string $table, array $keys, array $values): string
	{
		return 'REPLACE INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $values) . ')';
	}

	//--------------------------------------------------------------------

	/**
	 * FROM tables
	 *
	 * Groups tables in FROM clauses if needed, so there is no confusion
	 * about operator precedence.
	 *
	 * Note: This is only used (and overridden) by MySQL and CUBRID.
	 *
	 * @return string
	 */
	protected function _fromTables(): string
	{
		return implode(', ', $this->QBFrom);
	}

	//--------------------------------------------------------------------

	/**
	 * Get UPDATE query string
	 *
	 * Compiles an update query and returns the sql
	 *
	 * @param boolean $reset TRUE: reset QB values; FALSE: leave QB values alone
	 *
	 * @return string
	 */
	public function getCompiledUpdate(bool $reset = true): string
	{
		if ($this->validateUpdate() === false)
		{
			return false;
		}

		$sql = $this->_update($this->QBFrom[0], $this->QBSet);

		if ($reset === true)
		{
			$this->resetWrite();
		}

		return $this->compileFinalQuery($sql);
	}

	//--------------------------------------------------------------------

	/**
	 * UPDATE
	 *
	 * Compiles an update string and runs the query.
	 *
	 * @param array   $set   An associative array of update values
	 * @param mixed   $where
	 * @param integer $limit
	 * @param boolean $test  Are we testing the code?
	 *
	 * @return boolean    TRUE on success, FALSE on failure
	 */
	public function update(array $set = null, $where = null, int $limit = null, bool $test = false): bool
	{
		if ($set !== null)
		{
			$this->set($set);
		}

		if ($this->validateUpdate() === false)
		{
			return false;
		}

		if ($where !== null)
		{
			$this->where($where);
		}

		if (! empty($limit))
		{
			if (! $this->canLimitWhereUpdates)
			{
				throw new DatabaseException('This driver does not allow LIMITs on UPDATE queries using WHERE.');
			}

			$this->limit($limit);
		}

		$sql = $this->_update($this->QBFrom[0], $this->QBSet);

		if (! $test)
		{
			$this->resetWrite();

			if ($this->db->query($sql, $this->binds, false))
			{
				// Clear our binds so we don't eat up memory
				$this->binds = [];

				return true;
			}

			return false;
		}

		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Update statement
	 *
	 * Generates a platform-specific update string from the supplied data
	 *
	 * @param string $table  the Table name
	 * @param array  $values the Update data
	 *
	 * @return string
	 */
	protected function _update(string $table, array $values): string
	{
		$valstr = [];

		foreach ($values as $key => $val)
		{
			$valstr[] = $key . ' = ' . $val;
		}

		return 'UPDATE ' . $table . ' SET ' . implode(', ', $valstr)
				. $this->compileWhereHaving('QBWhere')
				. $this->compileOrderBy()
				. ($this->QBLimit ? $this->_limit(' ') : '');
	}

	//--------------------------------------------------------------------

	/**
	 * Validate Update
	 *
	 * This method is used by both update() and getCompiledUpdate() to
	 * validate that data is actually being set and that a table has been
	 * chosen to be update.
	 *
	 * @return boolean
	 * @throws \CodeIgniter\Database\Exceptions\DatabaseException
	 */
	protected function validateUpdate(): bool
	{
		if (empty($this->QBSet))
		{
			if (CI_DEBUG)
			{
				throw new DatabaseException('You must use the "set" method to update an entry.');
			}

			return false;
		}

		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Update_Batch
	 *
	 * Compiles an update string and runs the query
	 *
	 * @param array   $set       An associative array of update values
	 * @param string  $index     The where key
	 * @param integer $batchSize The size of the batch to run
	 * @param boolean $returnSQL True means SQL is returned, false will execute the query
	 *
	 * @return mixed    Number of rows affected, SQL string, or FALSE on failure
	 * @throws \CodeIgniter\Database\Exceptions\DatabaseException
	 */
	public function updateBatch(array $set = null, string $index = null, int $batchSize = 100, bool $returnSQL = false)
	{
		if ($index === null)
		{
			if (CI_DEBUG)
			{
				throw new DatabaseException('You must specify an index to match on for batch updates.');
			}

			return false;
		}

		if ($set === null)
		{
			if (empty($this->QBSet))
			{
				if (CI_DEBUG)
				{
					throw new DatabaseException('You must use the "set" method to update an entry.');
				}
				return false;
			}
		}
		else
		{
			if (empty($set))
			{
				if (CI_DEBUG)
				{
					throw new DatabaseException('updateBatch() called with no data');
				}
				return false;
			}

			$this->setUpdateBatch($set, $index);
		}

		$table = $this->QBFrom[0];

		// Batch this baby
		$affected_rows = 0;
		$savedSQL = [];
		for ($i = 0, $total = count($this->QBSet); $i < $total; $i += $batchSize)
		{
			$sql = $this->_updateBatch($table, array_slice($this->QBSet, $i, $batchSize), $this->db->protectIdentifiers($index)
			);

			if ($returnSQL)
			{
				$savedSQL[] = $sql;
			}
			else
			{
				$this->db->query($sql, $this->binds, false);
				$affected_rows += $this->db->affectedRows();
			}

			$this->QBWhere = [];
		}

		$this->resetWrite();

		return $returnSQL ? $savedSQL : $affected_rows;
	}

	//--------------------------------------------------------------------

	/**
	 * Update_Batch statement
	 *
	 * Generates a platform-specific batch update string from the supplied data
	 *
	 * @param string $table  Table name
	 * @param array  $values Update data
	 * @param string $index  WHERE key
	 *
	 * @return string
	 */
	protected function _updateBatch(string $table, array $values, string $index): string
	{
		$ids = [];
		$final = [];
		foreach ($values as $key => $val)
		{
			$ids[] = $val[$index];

			foreach (array_keys($val) as $field)
			{
				if ($field !== $index)
				{
					$final[$field][] = 'WHEN ' . $index . ' = ' . $val[$index] . ' THEN ' . $val[$field];
				}
			}
		}

		$cases = '';
		foreach ($final as $k => $v)
		{
			$cases .= $k . " = CASE \n"
					. implode("\n", $v) . "\n"
					. 'ELSE ' . $k . ' END, ';
		}

		$this->where($index . ' IN(' . implode(',', $ids) . ')', null, false);

		return 'UPDATE ' . $table . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere');
	}

	//--------------------------------------------------------------------

	/**
	 * The "setUpdateBatch" function.  Allows key/value pairs to be set for batch updating
	 *
	 * @param array|object   $key
	 * @param string         $index
	 * @param boolean        $escape
	 *
	 * @return BaseBuilder|null
	 * @throws \CodeIgniter\Database\Exceptions\DatabaseException
	 */
	public function setUpdateBatch($key, string $index = '', bool $escape = null)
	{
		$key = $this->batchObjectToArray($key);

		if (! is_array($key))
		{
			return null;
		}

		is_bool($escape) || $escape = $this->db->protectIdentifiers;

		foreach ($key as $k => $v)
		{
			$index_set = false;
			$clean     = [];
			foreach ($v as $k2 => $v2)
			{
				if ($k2 === $index)
				{
					$index_set = true;
				}

				$bind = $this->setBind($k2, $v2, $escape);

				$clean[$this->db->protectIdentifiers($k2, false, $escape)] = ":$bind:";
			}

			if ($index_set === false)
			{
				throw new DatabaseException('One or more rows submitted for batch updating is missing the specified index.');
			}

			$this->QBSet[] = $clean;
		}

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Empty Table
	 *
	 * Compiles a delete string and runs "DELETE FROM table"
	 *
	 * @param  boolean $test
	 * @return boolean    TRUE on success, FALSE on failure
	 */
	public function emptyTable(bool $test = false)
	{
		$table = $this->QBFrom[0];

		$sql = $this->_delete($table);

		if ($test)
		{
			return $sql;
		}

		$this->resetWrite();

		return $this->db->query($sql, null, false);
	}

	//--------------------------------------------------------------------

	/**
	 * Truncate
	 *
	 * Compiles a truncate string and runs the query
	 * If the database does not support the truncate() command
	 * This function maps to "DELETE FROM table"
	 *
	 * @param boolean $test Whether we're in test mode or not.
	 *
	 * @return boolean    TRUE on success, FALSE on failure
	 */
	public function truncate(bool $test = false)
	{
		$table = $this->QBFrom[0];

		$sql = $this->_truncate($table);

		if ($test === true)
		{
			return $sql;
		}

		$this->resetWrite();

		return $this->db->query($sql, null, false);
	}

	//--------------------------------------------------------------------

	/**
	 * Truncate statement
	 *
	 * Generates a platform-specific truncate string from the supplied data
	 *
	 * If the database does not support the truncate() command,
	 * then this method maps to 'DELETE FROM table'
	 *
	 * @param string $table The table name
	 *
	 * @return string
	 */
	protected function _truncate(string $table): string
	{
		return 'TRUNCATE ' . $table;
	}

	//--------------------------------------------------------------------

	/**
	 * Get DELETE query string
	 *
	 * Compiles a delete query string and returns the sql
	 *
	 * @param boolean $reset TRUE: reset QB values; FALSE: leave QB values alone
	 *
	 * @return string
	 */
	public function getCompiledDelete(bool $reset = true): string
	{
		$table = $this->QBFrom[0];

		$sql = $this->delete($table, '', $reset, true);

		return $this->compileFinalQuery($sql);
	}

	//--------------------------------------------------------------------

	/**
	 * Delete
	 *
	 * Compiles a delete string and runs the query
	 *
	 * @param mixed   $where      The where clause
	 * @param integer $limit      The limit clause
	 * @param boolean $reset_data
	 * @param boolean $returnSQL
	 *
	 * @return mixed
	 * @throws \CodeIgniter\Database\Exceptions\DatabaseException
	 */
	public function delete($where = '', int $limit = null, bool $reset_data = true, bool $returnSQL = false)
	{
		$table = $this->db->protectIdentifiers($this->QBFrom[0], true, null, false);

		if ($where !== '')
		{
			$this->where($where);
		}

		if (empty($this->QBWhere))
		{
			if (CI_DEBUG)
			{
				throw new DatabaseException('Deletes are not allowed unless they contain a "where" or "like" clause.');
			}

			return false;
		}

		$sql = $this->_delete($table);

		if (! empty($limit))
		{
			$this->QBLimit = $limit;
		}

		if (! empty($this->QBLimit))
		{
			if (! $this->canLimitDeletes)
			{
				throw new DatabaseException('SQLite3 does not allow LIMITs on DELETE queries.');
			}

			$sql = $this->_limit($sql);
		}

		if ($reset_data)
		{
			$this->resetWrite();
		}

		return ($returnSQL === true) ? $sql : $this->db->query($sql, $this->binds, false);
	}

	//--------------------------------------------------------------------

	/**
	 * Increments a numeric column by the specified value.
	 *
	 * @param string  $column
	 * @param integer $value
	 *
	 * @return boolean
	 */
	public function increment(string $column, int $value = 1)
	{
		$column = $this->db->protectIdentifiers($column);

		$sql = $this->_update($this->QBFrom[0], [$column => "{$column} + {$value}"]);

		return $this->db->query($sql, $this->binds, false);
	}

	//--------------------------------------------------------------------

	/**
	 * Decrements a numeric column by the specified value.
	 *
	 * @param string  $column
	 * @param integer $value
	 *
	 * @return boolean
	 */
	public function decrement(string $column, int $value = 1)
	{
		$column = $this->db->protectIdentifiers($column);

		$sql = $this->_update($this->QBFrom[0], [$column => "{$column}-{$value}"]);

		return $this->db->query($sql, $this->binds, false);
	}

	//--------------------------------------------------------------------

	/**
	 * Delete statement
	 *
	 * Generates a platform-specific delete string from the supplied data
	 *
	 * @param string $table The table name
	 *
	 * @return string
	 */
	protected function _delete(string $table): string
	{
		return 'DELETE FROM ' . $table . $this->compileWhereHaving('QBWhere')
				. ($this->QBLimit ? ' LIMIT ' . $this->QBLimit : '');
	}

	//--------------------------------------------------------------------

	/**
	 * Track Aliases
	 *
	 * Used to track SQL statements written with aliased tables.
	 *
	 * @param string|array $table The table to inspect
	 *
	 * @return string|void
	 */
	protected function trackAliases($table)
	{
		if (is_array($table))
		{
			foreach ($table as $t)
			{
				$this->trackAliases($t);
			}
			return;
		}

		// Does the string contain a comma?  If so, we need to separate
		// the string into discreet statements
		if (strpos($table, ',') !== false)
		{
			return $this->trackAliases(explode(',', $table));
		}

		// if a table alias is used we can recognize it by a space
		if (strpos($table, ' ') !== false)
		{
			// if the alias is written with the AS keyword, remove it
			$table = preg_replace('/\s+AS\s+/i', ' ', $table);

			// Grab the alias
			$table = trim(strrchr($table, ' '));

			// Store the alias, if it doesn't already exist
			$this->db->addTableAlias($table);
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Compile the SELECT statement
	 *
	 * Generates a query string based on which functions were used.
	 * Should not be called directly.
	 *
	 * @param mixed $select_override
	 *
	 * @return string
	 */
	protected function compileSelect($select_override = false): string
	{
		// Write the "select" portion of the query
		if ($select_override !== false)
		{
			$sql = $select_override;
		}
		else
		{
			$sql = ( ! $this->QBDistinct) ? 'SELECT ' : 'SELECT DISTINCT ';

			if (empty($this->QBSelect))
			{
				$sql .= '*';
			}
			else
			{
				// Cycle through the "select" portion of the query and prep each column name.
				// The reason we protect identifiers here rather than in the select() function
				// is because until the user calls the from() function we don't know if there are aliases
				foreach ($this->QBSelect as $key => $val)
				{
					$no_escape            = $this->QBNoEscape[$key] ?? null;
					$this->QBSelect[$key] = $this->db->protectIdentifiers($val, false, $no_escape);
				}

				$sql .= implode(', ', $this->QBSelect);
			}
		}

		// Write the "FROM" portion of the query
		if (! empty($this->QBFrom))
		{
			$sql .= "\nFROM " . $this->_fromTables();
		}

		// Write the "JOIN" portion of the query
		if (! empty($this->QBJoin))
		{
			$sql .= "\n" . implode("\n", $this->QBJoin);
		}

		$sql .= $this->compileWhereHaving('QBWhere')
				. $this->compileGroupBy()
				. $this->compileWhereHaving('QBHaving')
				. $this->compileOrderBy(); // ORDER BY
		// LIMIT
		if ($this->QBLimit)
		{
			return $this->_limit($sql . "\n");
		}

		return $sql;
	}

	//--------------------------------------------------------------------

	/**
	 * Compile WHERE, HAVING statements
	 *
	 * Escapes identifiers in WHERE and HAVING statements at execution time.
	 *
	 * Required so that aliases are tracked properly, regardless of whether
	 * where(), orWhere(), having(), orHaving are called prior to from(),
	 * join() and prefixTable is added only if needed.
	 *
	 * @param string $qb_key 'QBWhere' or 'QBHaving'
	 *
	 * @return string    SQL statement
	 */
	protected function compileWhereHaving(string $qb_key): string
	{
		if (! empty($this->$qb_key))
		{
			for ($i = 0, $c = count($this->$qb_key); $i < $c; $i ++)
			{
				// Is this condition already compiled?
				if (is_string($this->{$qb_key}[$i]))
				{
					continue;
				}
				elseif ($this->{$qb_key}[$i]['escape'] === false)
				{
					$this->{$qb_key}[$i] = $this->{$qb_key}[$i]['condition'];
					continue;
				}

				// Split multiple conditions
				$conditions = preg_split(
						'/((?:^|\s+)AND\s+|(?:^|\s+)OR\s+)/i', $this->{$qb_key}[$i]['condition'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
				);

				for ($ci = 0, $cc = count($conditions); $ci < $cc; $ci ++)
				{
					if (($op = $this->getOperator($conditions[$ci])) === false
							|| ! preg_match('/^(\(?)(.*)(' . preg_quote($op, '/') . ')\s*(.*(?<!\)))?(\)?)$/i', $conditions[$ci], $matches)
					)
					{
						continue;
					}

					// $matches = array(
					//	0 => '(test <= foo)',	/* the whole thing */
					//	1 => '(',		/* optional */
					//	2 => 'test',		/* the field name */
					//	3 => ' <= ',		/* $op */
					//	4 => 'foo',		/* optional, if $op is e.g. 'IS NULL' */
					//	5 => ')'		/* optional */
					// );

					if (! empty($matches[4]))
					{
						$matches[4] = ' ' . $matches[4];
					}

					$conditions[$ci] = $matches[1] . $this->db->protectIdentifiers(trim($matches[2]))
							. ' ' . trim($matches[3]) . $matches[4] . $matches[5];
				}

				$this->{$qb_key}[$i] = implode('', $conditions);
			}

			return ($qb_key === 'QBHaving' ? "\nHAVING " : "\nWHERE ")
					. implode("\n", $this->$qb_key);
		}

		return '';
	}

	//--------------------------------------------------------------------

	/**
	 * Compile GROUP BY
	 *
	 * Escapes identifiers in GROUP BY statements at execution time.
	 *
	 * Required so that aliases are tracked properly, regardless of whether
	 * groupBy() is called prior to from(), join() and prefixTable is added
	 * only if needed.
	 *
	 * @return string    SQL statement
	 */
	protected function compileGroupBy(): string
	{
		if (! empty($this->QBGroupBy))
		{
			for ($i = 0, $c = count($this->QBGroupBy); $i < $c; $i ++)
			{
				// Is it already compiled?
				if (is_string($this->QBGroupBy[$i]))
				{
					continue;
				}

				$this->QBGroupBy[$i] = ($this->QBGroupBy[$i]['escape'] === false ||
						$this->isLiteral($this->QBGroupBy[$i]['field'])) ? $this->QBGroupBy[$i]['field'] : $this->db->protectIdentifiers($this->QBGroupBy[$i]['field']);
			}

			return "\nGROUP BY " . implode(', ', $this->QBGroupBy);
		}

		return '';
	}

	//--------------------------------------------------------------------

	/**
	 * Compile ORDER BY
	 *
	 * Escapes identifiers in ORDER BY statements at execution time.
	 *
	 * Required so that aliases are tracked properly, regardless of whether
	 * orderBy() is called prior to from(), join() and prefixTable is added
	 * only if needed.
	 *
	 * @return string    SQL statement
	 */
	protected function compileOrderBy(): string
	{
		if (is_array($this->QBOrderBy) && ! empty($this->QBOrderBy))
		{
			for ($i = 0, $c = count($this->QBOrderBy); $i < $c; $i ++)
			{
				if ($this->QBOrderBy[$i]['escape'] !== false && ! $this->isLiteral($this->QBOrderBy[$i]['field']))
				{
					$this->QBOrderBy[$i]['field'] = $this->db->protectIdentifiers($this->QBOrderBy[$i]['field']);
				}

				$this->QBOrderBy[$i] = $this->QBOrderBy[$i]['field'] . $this->QBOrderBy[$i]['direction'];
			}

			return $this->QBOrderBy = "\nORDER BY " . implode(', ', $this->QBOrderBy);
		}
		elseif (is_string($this->QBOrderBy))
		{
			return $this->QBOrderBy;
		}

		return '';
	}

	//--------------------------------------------------------------------

	/**
	 * Object to Array
	 *
	 * Takes an object as input and converts the class variables to array key/vals
	 *
	 * @param mixed $object
	 *
	 * @return mixed
	 */
	protected function objectToArray($object)
	{
		if (! is_object($object))
		{
			return $object;
		}

		$array = [];
		foreach (get_object_vars($object) as $key => $val)
		{
			// There are some built in keys we need to ignore for this conversion
			if (! is_object($val) && ! is_array($val) && $key !== '_parent_name')
			{
				$array[$key] = $val;
			}
		}

		return $array;
	}

	//--------------------------------------------------------------------

	/**
	 * Object to Array
	 *
	 * Takes an object as input and converts the class variables to array key/vals
	 *
	 * @param mixed $object
	 *
	 * @return mixed
	 */
	protected function batchObjectToArray($object)
	{
		if (! is_object($object))
		{
			return $object;
		}

		$array  = [];
		$out    = get_object_vars($object);
		$fields = array_keys($out);

		foreach ($fields as $val)
		{
			// There are some built in keys we need to ignore for this conversion
			if ($val !== '_parent_name')
			{
				$i = 0;
				foreach ($out[$val] as $data)
				{
					$array[$i ++][$val] = $data;
				}
			}
		}

		return $array;
	}

	//--------------------------------------------------------------------

	/**
	 * Is literal
	 *
	 * Determines if a string represents a literal value or a field name
	 *
	 * @param string $str
	 *
	 * @return boolean
	 */
	protected function isLiteral(string $str): bool
	{
		$str = trim($str);

		if (empty($str) || ctype_digit($str) || (string) (float) $str === $str ||
				in_array(strtoupper($str), ['TRUE', 'FALSE'], true)
		)
		{
			return true;
		}

		static $_str;

		if (empty($_str))
		{
			$_str = ($this->db->escapeChar !== '"') ? ['"', "'"] : ["'"];
		}

		return in_array($str[0], $_str, true);
	}

	//--------------------------------------------------------------------

	/**
	 * Reset Query Builder values.
	 *
	 * Publicly-visible method to reset the QB values.
	 *
	 * @return BaseBuilder
	 */
	public function resetQuery()
	{
		$this->resetSelect();
		$this->resetWrite();

		return $this;
	}

	//--------------------------------------------------------------------

	/**
	 * Resets the query builder values.  Called by the get() function
	 *
	 * @param array $qb_reset_items An array of fields to reset
	 *
	 * @return void
	 */
	protected function resetRun(array $qb_reset_items)
	{
		foreach ($qb_reset_items as $item => $default_value)
		{
			$this->$item = $default_value;
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Resets the query builder values.  Called by the get() function
	 */
	protected function resetSelect()
	{
		$this->resetRun([
			'QBSelect'   => [],
			'QBJoin'     => [],
			'QBWhere'    => [],
			'QBGroupBy'  => [],
			'QBHaving'   => [],
			'QBOrderBy'  => [],
			'QBNoEscape' => [],
			'QBDistinct' => false,
			'QBLimit'    => false,
			'QBOffset'   => false,
		]);

		if (! empty($this->db))
		{
			$this->db->setAliasedTables([]);
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Resets the query builder "write" values.
	 *
	 * Called by the insert() update() insertBatch() updateBatch() and delete() functions
	 */
	protected function resetWrite()
	{
		$this->resetRun([
			'QBSet'     => [],
			'QBJoin'    => [],
			'QBWhere'   => [],
			'QBOrderBy' => [],
			'QBKeys'    => [],
			'QBLimit'   => false,
		]);
	}

	//--------------------------------------------------------------------

	/**
	 * Tests whether the string has an SQL operator
	 *
	 * @param string $str
	 *
	 * @return boolean
	 */
	protected function hasOperator(string $str): bool
	{
		return (bool) preg_match('/(<|>|!|=|\sIS NULL|\sIS NOT NULL|\sEXISTS|\sBETWEEN|\sLIKE|\sIN\s*\(|\s)/i', trim($str));
	}

	// --------------------------------------------------------------------

	/**
	 * Returns the SQL string operator
	 *
	 * @param string  $str
	 * @param boolean $list
	 *
	 * @return mixed
	 */
	protected function getOperator(string $str, bool $list = false)
	{
		static $_operators;

		if (empty($_operators))
		{
			$_les       = ($this->db->likeEscapeStr !== '') ? '\s+' . preg_quote(trim(sprintf($this->db->likeEscapeStr, $this->db->likeEscapeChar)), '/') : '';
			$_operators = [
				'\s*(?:<|>|!)?=\s*', // =, <=, >=, !=
				'\s*<>?\s*', // <, <>
				'\s*>\s*', // >
				'\s+IS NULL', // IS NULL
				'\s+IS NOT NULL', // IS NOT NULL
				'\s+EXISTS\s*\(.*\)', // EXISTS(sql)
				'\s+NOT EXISTS\s*\(.*\)', // NOT EXISTS(sql)
				'\s+BETWEEN\s+', // BETWEEN value AND value
				'\s+IN\s*\(.*\)', // IN(list)
				'\s+NOT IN\s*\(.*\)', // NOT IN (list)
				'\s+LIKE\s+\S.*(' . $_les . ')?', // LIKE 'expr'[ ESCAPE '%s']
				'\s+NOT LIKE\s+\S.*(' . $_les . ')?', // NOT LIKE 'expr'[ ESCAPE '%s']
			];
		}

		return preg_match_all('/' . implode('|', $_operators) . '/i', $str, $match) ? ($list ? $match[0] : $match[0][count($match[0]) - 1]) : false;
	}

	// --------------------------------------------------------------------

	/**
	 * Stores a bind value after ensuring that it's unique.
	 * While it might be nicer to have named keys for our binds array
	 * with PHP 7+ we get a huge memory/performance gain with indexed
	 * arrays instead, so lets take advantage of that here.
	 *
	 * @param string  $key
	 * @param mixed   $value
	 * @param boolean $escape
	 *
	 * @return string
	 */
	protected function setBind(string $key, $value = null, bool $escape = true): string
	{
		if (! array_key_exists($key, $this->binds))
		{
			$this->binds[$key] = [
				$value,
				$escape,
			];

			return $key;
		}

		$count = 0;

		while (array_key_exists($key . $count, $this->binds))
		{
			++$count;
		}

		$this->binds[$key . $count] = [
			$value,
			$escape,
		];

		return $key . $count;
	}

	//--------------------------------------------------------------------
}


<?php
/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Debug;

use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Debug\Toolbar\Collectors\History;
use CodeIgniter\Format\JSONFormatter;
use CodeIgniter\Format\XMLFormatter;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;

/**
 * Debug Toolbar
 *
 * Displays a toolbar with bits of stats to aid a developer in debugging.
 *
 * Inspiration: http://prophiler.fabfuel.de
 *
 * @package CodeIgniter\Debug
 */
class Toolbar
{
	/**
	 * @var BaseConfig
	 */
	protected $config;

	/**
	 * Collectors to be used and displayed.
	 *
	 * @var \CodeIgniter\Debug\Toolbar\Collectors\BaseCollector[]
	 */
	protected $collectors = [];

	//--------------------------------------------------------------------

	/**
	 * Constructor
	 *
	 * @param BaseConfig $config
	 */
	public function __construct(BaseConfig $config)
	{
		$this->config = $config;

		foreach ($config->collectors as $collector)
		{
			if (! class_exists($collector))
			{
				log_message('critical', 'Toolbar collector does not exists(' . $collector . ').' .
					'please check $collectors in the Config\Toolbar.php file.');
				continue;
			}

			$this->collectors[] = new $collector();
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Returns all the data required by Debug Bar
	 *
	 * @param float                               $startTime App start time
	 * @param float                               $totalTime
	 * @param \CodeIgniter\HTTP\RequestInterface  $request
	 * @param \CodeIgniter\HTTP\ResponseInterface $response
	 *
	 * @return string JSON encoded data
	 */
	public function run(float $startTime, float $totalTime, RequestInterface $request, ResponseInterface $response): string
	{
		// Data items used within the view.
		$data['url']             = current_url();
		$data['method']          = $request->getMethod(true);
		$data['isAJAX']          = $request->isAJAX();
		$data['startTime']       = $startTime;
		$data['totalTime']       = $totalTime * 1000;
		$data['totalMemory']     = number_format((memory_get_peak_usage()) / 1024 / 1024, 3);
		$data['segmentDuration'] = $this->roundTo($data['totalTime'] / 7, 5);
		$data['segmentCount']    = (int)ceil($data['totalTime'] / $data['segmentDuration']);
		$data['CI_VERSION']      = \CodeIgniter\CodeIgniter::CI_VERSION;
		$data['collectors']      = [];

		foreach ($this->collectors as $collector)
		{
			$data['collectors'][] = $collector->getAsArray();
		}

		foreach ($this->collectVarData() as $heading => $items)
		{
			$varData = [];

			if (is_array($items))
			{
				foreach ($items as $key => $value)
				{
					$varData[esc($key)] = is_string($value) ? esc($value) : print_r($value, true);
				}
			}

			$data['vars']['varData'][esc($heading)] = $varData;
		}

		if (! empty($_SESSION))
		{
			foreach ($_SESSION as $key => $value)
			{
				// Replace the binary data with string to avoid json_encode failure.
				if (is_string($value) && preg_match('~[^\x20-\x7E\t\r\n]~', $value))
				{
					$value = 'binary data';
				}

				$data['vars']['session'][esc($key)] = is_string($value) ? esc($value) : print_r($value, true);
			}
		}

		foreach ($request->getGet() as $name => $value)
		{
			$data['vars']['get'][esc($name)] = is_array($value) ? esc(print_r($value, true)) : esc($value);
		}

		foreach ($request->getPost() as $name => $value)
		{
			$data['vars']['post'][esc($name)] = is_array($value) ? esc(print_r($value, true)) : esc($value);
		}

		foreach ($request->getHeaders() as $header => $value)
		{
			if (empty($value))
			{
				continue;
			}

			if (! is_array($value))
			{
				$value = [$value];
			}

			foreach ($value as $h)
			{
				$data['vars']['headers'][esc($h->getName())] = esc($h->getValueLine());
			}
		}

		foreach ($request->getCookie() as $name => $value)
		{
			$data['vars']['cookies'][esc($name)] = esc($value);
		}

		$data['vars']['request'] = ($request->isSecure() ? 'HTTPS' : 'HTTP') . '/' . $request->getProtocolVersion();

		$data['vars']['response'] = [
			'statusCode'  => $response->getStatusCode(),
			'reason'      => esc($response->getReason()),
			'contentType' => esc($response->getHeaderLine('content-type')),
		];

		$data['config'] = \CodeIgniter\Debug\Toolbar\Collectors\Config::display();

		if ($response->CSP !== null)
		{
			$response->CSP->addImageSrc( 'data:' );
		}

		return json_encode($data);
	}

	//--------------------------------------------------------------------

	//--------------------------------------------------------------------

	/**
	 * Called within the view to display the timeline itself.
	 *
	 * @param array   $collectors
	 * @param float   $startTime
	 * @param integer $segmentCount
	 * @param integer $segmentDuration
	 * @param array   $styles
	 *
	 * @return string
	 */
	protected function renderTimeline(array $collectors, $startTime, int $segmentCount, int $segmentDuration, array &$styles): string
	{
		$displayTime = $segmentCount * $segmentDuration;
		$rows = $this->collectTimelineData($collectors);
		$output = '';
		$styleCount = 0;

		foreach ($rows as $row)
		{
			$output .= '
';
			$output .= "{$row['name']}	";
			$output .= "{$row['component']}	";
			$output .= "" . number_format($row['duration'] * 1000, 2) . ' ms	';
			$output .= "";

			$offset = ((($row['start'] - $startTime) * 1000) / $displayTime) * 100;
			$length = (($row['duration'] * 1000) / $displayTime) * 100;

			$styles['debug-bar-timeline-' . $styleCount] = "left: {$offset}%; width: {$length}%;";
			$output .= "";
			$output .= '	';
			$output .= '
';

			$styleCount++;
		}

		return $output;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns a sorted array of timeline data arrays from the collectors.
	 *
	 * @param array $collectors
	 *
	 * @return array
	 */
	protected function collectTimelineData($collectors): array
	{
		$data = [];

		// Collect it
		foreach ($collectors as $collector)
		{
			if (! $collector['hasTimelineData'])
			{
				continue;
			}

			$data = array_merge($data, $collector['timelineData']);
		}

		// Sort it

		return $data;
	}

	//--------------------------------------------------------------------

	/**
	 * Returns an array of data from all of the modules
	 * that should be displayed in the 'Vars' tab.
	 *
	 * @return array
	 */
	protected function collectVarData(): array
	{
		$data = [];

		foreach ($this->collectors as $collector)
		{
			if (! $collector->hasVarData())
			{
				continue;
			}

			$data = array_merge($data, $collector->getVarData());
		}

		return $data;
	}

	//--------------------------------------------------------------------

	/**
	 * Rounds a number to the nearest incremental value.
	 *
	 * @param float   $number
	 * @param integer $increments
	 *
	 * @return float
	 */
	protected function roundTo(float $number, int $increments = 5): float
	{
		$increments = 1 / $increments;

		return (ceil($number * $increments) / $increments);
	}

	//--------------------------------------------------------------------

	public function prepare()
	{
		if (CI_DEBUG && ! is_cli())
		{
			global $app;

			$request  = Services::request();
			$response = Services::response();

			$toolbar = Services::toolbar(config(Toolbar::class));
			$stats   = $app->getPerformanceStats();
			$data    = $toolbar->run(
				$stats['startTime'],
				$stats['totalTime'],
				$request,
				$response
			);

			helper('filesystem');

			// Updated to time() so we can get history
			$time = time();

			if (! is_dir(WRITEPATH . 'debugbar'))
			{
				mkdir(WRITEPATH . 'debugbar', 0777);
			}

			write_file(WRITEPATH . 'debugbar/' . 'debugbar_' . $time . '.json', $data, 'w+');

			$format = $response->getHeaderLine('content-type');

			// Non-HTML formats should not include the debugbar
			// then we send headers saying where to find the debug data
			// for this response
			if ($request->isAJAX() || strpos($format, 'html') === false)
			{
				$response->setHeader('Debugbar-Time', $time)
						 ->setHeader('Debugbar-Link', site_url("?debugbar_time={$time}"))
						 ->getBody();

				return;
			}

			$script = PHP_EOL
				. ''
				. ''
				. ''
				. PHP_EOL;

			if (strpos($response->getBody(), '') !== false)
			{
				$response->setBody(
					str_replace('', $script . '', $response->getBody())
				);

				return;
			}

			$response->appendBody($script);
		}
	}

	//--------------------------------------------------------------------

	/**
	 *
	 */
	public function respond()
	{
		if (ENVIRONMENT === 'testing')
		{
			return;
		}

		$request = Services::request();

		// If the request contains '?debugbar then we're
		// simply returning the loading script
		if ($request->getGet('debugbar') !== null)
		{
			// Let the browser know that we are sending javascript
			header('Content-Type: application/javascript');

			ob_start();
			include($this->config->viewsPath . 'toolbarloader.js.php');
			$output = ob_get_clean();

			exit($output);
		}

		// Otherwise, if it includes ?debugbar_time, then
		// we should return the entire debugbar.
		if ($request->getGet('debugbar_time'))
		{
			helper('security');

			// Negotiate the content-type to format the output
			$format = $request->negotiate('media', [
				'text/html',
				'application/json',
				'application/xml',
			]);
			$format = explode('/', $format)[1];

			$file     = sanitize_filename('debugbar_' . $request->getGet('debugbar_time'));
			$filename = WRITEPATH . 'debugbar/' . $file . '.json';

			// Show the toolbar
			if (is_file($filename))
			{
				$contents = $this->format(file_get_contents($filename), $format);
				exit($contents);
			}

			// File was not written or do not exists
			http_response_code(404);
			exit; // Exit here is needed to avoid load the index page
		}
	}

	/**
	 * Format output
	 *
	 * @param string $data   JSON encoded Toolbar data
	 * @param string $format html, json, xml
	 *
	 * @return string
	 */
	protected function format(string $data, string $format = 'html'): string
	{
		$data = json_decode($data, true);

		if ($this->config->maxHistory !== 0)
		{
			$history = new History();
			$history->setFiles(
				Services::request()->getGet('debugbar_time'),
				$this->config->maxHistory
			);

			$data['collectors'][] = $history->getAsArray();
		}

		$output = '';

		switch ($format)
		{
			case 'html':
				$data['styles'] = [];
				extract($data);
				$parser = Services::parser($this->config->viewsPath, null, false);
				ob_start();
				include($this->config->viewsPath . 'toolbar.tpl.php');
				$output = ob_get_clean();
				break;
			case 'json':
				$formatter = new JSONFormatter();
				$output    = $formatter->format($data);
				break;
			case 'xml':
				$formatter = new XMLFormatter;
				$output    = $formatter->format($data);
				break;
		}

		return $output;
	}
}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT    MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Images;

use CodeIgniter\Files\File;
use CodeIgniter\Images\Exceptions\ImageException;

/**
 * Encapsulation of an Image file
 */
class Image extends File
{

	/**
	 * The original image width in pixels.
	 *
	 * @var
	 */
	public $origWidth;

	/**
	 * The original image height in pixels.
	 *
	 * @var
	 */
	public $origHeight;

	/**
	 * The image type constant.
	 *
	 * @see http://php.net/manual/en/image.constants.php
	 *
	 * @var integer
	 */
	public $imageType;

	/**
	 * attributes string with size info:
	 * 'height="100" width="200"'
	 *
	 * @var string
	 */
	public $sizeStr;

	/**
	 * The image's mime type, i.e. image/jpeg
	 *
	 * @var string
	 */
	public $mime;

	/**
	 * Makes a copy of itself to the new location. If no filename is provided
	 * it will use the existing filename.
	 *
	 * @param string      $targetPath The directory to store the file in
	 * @param string|null $targetName The new name of the copied file.
	 * @param integer     $perms      File permissions to be applied after copy.
	 *
	 * @return boolean
	 */
	public function copy(string $targetPath, string $targetName = null, int $perms = 0644): bool
	{
		$targetPath = rtrim($targetPath, '/ ') . '/';

		$targetName = is_null($targetName) ? $this->getFilename() : $targetName;

		if (empty($targetName))
		{
			throw ImageException::forInvalidFile($targetName);
		}

		if (! is_dir($targetPath))
		{
			mkdir($targetName, 0755, true);
		}

		if (! copy($this->getPathname(), "{$targetPath}{$targetName}"))
		{
			throw ImageException::forCopyError($targetPath);
		}

		chmod("{$targetPath}/{$targetName}", $perms);

		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Get image properties
	 *
	 * A helper function that gets info about the file
	 *
	 * @param boolean $return
	 *
	 * @return mixed
	 */
	public function getProperties(bool $return = false)
	{
		$path = $this->getPathname();

		$vals  = getimagesize($path);
		$types = [
			1 => 'gif',
			2 => 'jpeg',
			3 => 'png',
		];

		$mime = 'image/' . ($types[$vals[2]] ?? 'jpg');

		if ($return === true)
		{
			return [
				'width'      => $vals[0],
				'height'     => $vals[1],
				'image_type' => $vals[2],
				'size_str'   => $vals[3],
				'mime_type'  => $mime,
			];
		}

		$this->origWidth  = $vals[0];
		$this->origHeight = $vals[1];
		$this->imageType  = $vals[2];
		$this->sizeStr    = $vals[3];
		$this->mime       = $mime;

		return true;
	}

	//--------------------------------------------------------------------
}


<?php

/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 3.0.0
 * @filesource
 */

namespace CodeIgniter\Session\Handlers;

use CodeIgniter\Session\Exceptions\SessionException;
use CodeIgniter\Config\BaseConfig;
use CodeIgniter\Database\BaseConnection;
use Config\Database;

/**
 * Session handler using current Database for storage
 */
class DatabaseHandler extends BaseHandler implements \SessionHandlerInterface
{

	/**
	 * The database group to use for storage.
	 *
	 * @var string
	 */
	protected $DBGroup;

	/**
	 * The name of the table to store session info.
	 *
	 * @var string
	 */
	protected $table;

	/**
	 * The DB Connection instance.
	 *
	 * @var BaseConnection
	 */
	protected $db;

	/**
	 * The database type, for locking purposes.
	 *
	 * @var string
	 */
	protected $platform;

	/**
	 * Row exists flag
	 *
	 * @var boolean
	 */
	protected $rowExists = false;

	//--------------------------------------------------------------------

	/**
	 * Constructor
	 *
	 * @param BaseConfig $config
	 */
	public function __construct(BaseConfig $config, string $ipAddress)
	{
		parent::__construct($config, $ipAddress);

		// Determine Table
		$this->table = $config->sessionSavePath;

		if (empty($this->table))
		{
			throw SessionException::forMissingDatabaseTable();
		}

		// Get DB Connection
		$this->DBGroup = $config->sessionDBGroup ?? config(Database::class)->defaultGroup;

		$this->db = Database::connect($this->DBGroup);

		// Determine Database type
		$driver = strtolower(get_class($this->db));
		if (strpos($driver, 'mysql') !== false)
		{
			$this->platform = 'mysql';
		}
		elseif (strpos($driver, 'postgre') !== false)
		{
			$this->platform = 'postgre';
		}
	}

	//--------------------------------------------------------------------

	/**
	 * Open
	 *
	 * Ensures we have an initialized database connection.
	 *
	 * @param string $savePath Path to session files' directory
	 * @param string $name     Session cookie name
	 *
	 * @return boolean
	 * @throws \Exception
	 */
	public function open($savePath, $name): bool
	{
		if (empty($this->db->connID))
		{
			$this->db->initialize();
		}

		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Read
	 *
	 * Reads session data and acquires a lock
	 *
	 * @param string $sessionID Session ID
	 *
	 * @return string    Serialized session data
	 */
	public function read($sessionID): string
	{
		if ($this->lockSession($sessionID) === false)
		{
			$this->fingerprint = md5('');
			return '';
		}

		// Needed by write() to detect session_regenerate_id() calls
		$this->sessionID = $sessionID;

		$builder = $this->db->table($this->table)
				->select('data')
				->where('id', $sessionID);

		if ($this->matchIP)
		{
			$builder = $builder->where('ip_address', $this->ipAddress);
		}

		$result = $builder->get()->getRow();

		if ($result === null)
		{
			// PHP7 will reuse the same SessionHandler object after
			// ID regeneration, so we need to explicitly set this to
			// FALSE instead of relying on the default ...
			$this->rowExists   = false;
			$this->fingerprint = md5('');

			return '';
		}

		// PostgreSQL's variant of a BLOB datatype is Bytea, which is a
		// PITA to work with, so we use base64-encoded data in a TEXT
		// field instead.
		if (is_bool($result))
		{
			$result = '';
		}
		else
		{
			$result = ($this->platform === 'postgre') ? base64_decode(rtrim($result->data)) : $result->data;
		}

		$this->fingerprint = md5($result);
		$this->rowExists   = true;

		return $result;
	}

	//--------------------------------------------------------------------

	/**
	 * Write
	 *
	 * Writes (create / update) session data
	 *
	 * @param string $sessionID   Session ID
	 * @param string $sessionData Serialized session data
	 *
	 * @return boolean
	 */
	public function write($sessionID, $sessionData): bool
	{
		if ($this->lock === false)
		{
			return $this->fail();
		}

		// Was the ID regenerated?
		elseif ($sessionID !== $this->sessionID)
		{
			if (! $this->releaseLock() || ! $this->lockSession($sessionID))
			{
				return $this->fail();
			}

			$this->rowExists = false;
			$this->sessionID = $sessionID;
		}

		if ($this->rowExists === false)
		{
			$insertData = [
				'id'         => $sessionID,
				'ip_address' => $this->ipAddress,
				'timestamp'  => time(),
				'data'       => $this->platform === 'postgre' ? base64_encode($sessionData) : $sessionData,
			];

			if (! $this->db->table($this->table)->insert($insertData))
			{
				return $this->fail();
			}

			$this->fingerprint = md5($sessionData);
			$this->rowExists   = true;

			return true;
		}

		$builder = $this->db->table($this->table)->where('id', $sessionID);

		if ($this->matchIP)
		{
			$builder = $builder->where('ip_address', $this->ipAddress);
		}

		$updateData = [
			'timestamp' => time(),
		];

		if ($this->fingerprint !== md5($sessionData))
		{
			$updateData['data'] = ($this->platform === 'postgre') ? base64_encode($sessionData) : $sessionData;
		}

		if (! $builder->update($updateData))
		{
			return $this->fail();
		}

		$this->fingerprint = md5($sessionData);

		return true;
	}

	//--------------------------------------------------------------------

	/**
	 * Close
	 *
	 * Releases locks and closes file descriptor.
	 *
	 * @return boolean
	 */
	public function close(): bool
	{
		return ($this->lock && ! $this->releaseLock()) ? $this->fail() : true;
	}

	//--------------------------------------------------------------------

	/**
	 * Destroy
	 *
	 * Destroys the current session.
	 *
	 * @param string $sessionID
	 *
	 * @return boolean
	 */
	public function destroy($sessionID): bool
	{
		if ($this->lock)
		{
			$builder = $this->db->table($this->table)->where('id', $sessionID);

			if ($this->matchIP)
			{
				$builder = $builder->where('ip_address', $this->ipAddress);
			}

			if (! $builder->delete())
			{
				return $this->fail();
			}
		}

		if ($this->close())
		{
			$this->destroyCookie();

			return true;
		}

		return $this->fail();
	}

	//--------------------------------------------------------------------

	/**
	 * Garbage Collector
	 *
	 * Deletes expired sessions
	 *
	 * @param integer $maxlifetime Maximum lifetime of sessions
	 *
	 * @return boolean
	 */
	public function gc($maxlifetime): bool
	{
		return ($this->db->table($this->table)->delete('timestamp < ' . (time() - $maxlifetime))) ? true : $this->fail();
	}

	//--------------------------------------------------------------------

	protected function lockSession(string $sessionID): bool
	{
		if ($this->platform === 'mysql')
		{
			$arg = md5($sessionID . ($this->matchIP ? '_' . $this->ipAddress : ''));
			if ($this->db->query("SELECT GET_LOCK('{$arg}', 300) AS ci_session_lock")->getRow()->ci_session_lock)
			{
				$this->lock = $arg;
				return true;
			}

			return $this->fail();
		}
		elseif ($this->platform === 'postgre')
		{
			$arg = "hashtext('{$sessionID}')" . ($this->matchIP ? ", hashtext('{$this->ipAddress}')" : '');
			if ($this->db->simpleQuery("SELECT pg_advisory_lock({$arg})"))
			{
				$this->lock = $arg;
				return true;
			}

			return $this->fail();
		}

		// Unsupported DB? Let the parent handle the simplified version.
		return parent::lockSession($sessionID);
	}

	//--------------------------------------------------------------------

	/**
	 * Releases the lock, if any.
	 *
	 * @return boolean
	 */
	protected function releaseLock(): bool
	{
		if (! $this->lock)
		{
			return true;
		}

		if ($this->platform === 'mysql')
		{
			if ($this->db->query("SELECT RELEASE_LOCK('{$this->lock}') AS ci_session_lock")->getRow()->ci_session_lock)
			{
				$this->lock = false;
				return true;
			}

			return $this->fail();
		}
		elseif ($this->platform === 'postgre')
		{
			if ($this->db->simpleQuery("SELECT pg_advisory_unlock({$this->lock})"))
			{
				$this->lock = false;
				return true;
			}

			return $this->fail();
		}

		// Unsupported DB? Let the parent handle the simple version.
		return parent::releaseLock();
	}

	//--------------------------------------------------------------------
}


<?php
/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014-2019 British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package    CodeIgniter
 * @author     CodeIgniter Dev Team
 * @copyright  2014-2019 British Columbia Institute of Technology (https://bcit.ca/)
 * @license    https://opensource.org/licenses/MIT	MIT License
 * @link       https://codeigniter.com
 * @since      Version 4.0.0
 * @filesource
 */

namespace CodeIgniter\Debug\Toolbar\Collectors;

use CodeIgniter\Config\Services;

/**
 * Routes collector
 */
class Routes extends BaseCollector
{

	/**
	 * Whether this collector has data that can
	 * be displayed in the Timeline.
	 *
	 * @var boolean
	 */
	protected $hasTimeline = false;

	/**
	 * Whether this collector needs to display
	 * content in a tab or not.
	 *
	 * @var boolean
	 */
	protected $hasTabContent = true;

	/**
	 * The 'title' of this Collector.
	 * Used to name things in the toolbar HTML.
	 *
	 * @var string
	 */
	protected $title = 'Routes';

	//--------------------------------------------------------------------

	/**
	 * Returns the data of this collector to be formatted in the toolbar
	 *
	 * @return array
	 * @throws \ReflectionException
	 */
	public function display(): array
	{
		$rawRoutes = Services::routes(true);
		$router    = Services::router(null, true);

		/*
		 * Matched Route
		 */
		$route = $router->getMatchedRoute();

		// Get our parameters
		$method    = is_callable($router->controllerName()) ? new \ReflectionFunction($router->controllerName()) : new \ReflectionMethod($router->controllerName(), $router->methodName());
		$rawParams = $method->getParameters();

		$params = [];
		foreach ($rawParams as $key => $param)
		{
			$params[] = [
				'name'  => $param->getName(),
				'value' => $router->params()[$key] ??
					'<empty>&nbsp| default: ' . var_export($param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, true),
			];
		}

		$matchedRoute = [
			[
				'directory'  => $router->directory(),
				'controller' => $router->controllerName(),
				'method'     => $router->methodName(),
				'paramCount' => count($router->params()),
				'truePCount' => count($params),
				'params'     => $params ?? [],
			],
		];

		/*
		 * Defined Routes
		 */
		$rawRoutes = $rawRoutes->getRoutes();
		$routes    = [];

		foreach ($rawRoutes as $from => $to)
		{
			$routes[] = [
				'from' => $from,
				'to'   => $to,
			];
		}

		return [
			'matchedRoute' => $matchedRoute,
			'routes'       => $routes,
		];
	}

	//--------------------------------------------------------------------

	/**
	 * Returns a count of all the routes in the system.
	 *
	 * @return integer
	 */
	public function getBadgeValue(): int
	{
		$rawRoutes = Services::routes(true);

		return count($rawRoutes->getRoutes());
	}

	//--------------------------------------------------------------------

	/**
	 * Display the icon.
	 *
	 * Icon from https://icons8.com - 1em package
	 *
	 * @return string
	 */
	public function icon(): string
	{
		return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFDSURBVEhL7ZRNSsNQFIUjVXSiOFEcuQIHDpzpxC0IGYeE/BEInbWlCHEDLsSiuANdhKDjgm6ggtSJ+l25ldrmmTwIgtgDh/t37r1J+16cX0dRFMtpmu5pWAkrvYjjOB7AETzStBFW+inxu3KUJMmhludQpoflS1zXban4LYqiO224h6VLTHr8Z+z8EpIHFF9gG78nDVmW7UgTHKjsCyY98QP+pcq+g8Ku2s8G8X3f3/I8b038WZTp+bO38zxfFd+I6YY6sNUvFlSDk9CRhiAI1jX1I9Cfw7GG1UB8LAuwbU0ZwQnbRDeEN5qqBxZMLtE1ti9LtbREnMIuOXnyIf5rGIb7Wq8HmlZgwYBH7ORTcKH5E4mpjeGt9fBZcHE2GCQ3Vt7oTNPNg+FXLHnSsHkw/FR+Gg2bB8Ptzrst/v6C/wrH+QB+duli6MYJdQAAAABJRU5ErkJggg==';
	}
}