You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1882 lines
41 KiB

<?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
* Copyright (c) 2019 CodeIgniter Foundation
*
* 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 2019 CodeIgniter Foundation
* @license https://opensource.org/licenses/MIT MIT License
* @link https://codeigniter.com
* @since Version 4.0.0
* @filesource
*/
namespace CodeIgniter\Database;
use CodeIgniter\Events\Events;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Class BaseConnection
*/
abstract class BaseConnection implements ConnectionInterface
{
/**
* Data Source Name / Connect string
*
* @var string
*/
protected $DSN;
/**
* Database port
*
* @var integer
*/
protected $port = '';
/**
* Hostname
*
* @var string
*/
protected $hostname;
/**
* Username
*
* @var string
*/
protected $username;
/**
* Password
*
* @var string
*/
protected $password;
/**
* Database name
*
* @var string
*/
protected $database;
/**
* Database driver
*
* @var string
*/
protected $DBDriver = 'MySQLi';
/**
* Sub-driver
*
* @used-by CI_DB_pdo_driver
* @var string
*/
protected $subdriver;
/**
* Table prefix
*
* @var string
*/
protected $DBPrefix = '';
/**
* Persistent connection flag
*
* @var boolean
*/
protected $pConnect = false;
/**
* Debug flag
*
* Whether to display error messages.
*
* @var boolean
*/
protected $DBDebug = false;
/**
* Should we cache results?
*
* @var boolean
*/
protected $cacheOn = false;
/**
* Path to store cache files.
*
* @var string
*/
protected $cacheDir;
/**
* Character set
*
* @var string
*/
protected $charset = 'utf8';
/**
* Collation
*
* @var string
*/
protected $DBCollat = 'utf8_general_ci';
/**
* Swap Prefix
*
* @var string
*/
protected $swapPre = '';
/**
* Encryption flag/data
*
* @var mixed
*/
protected $encrypt = false;
/**
* Compression flag
*
* @var boolean
*/
protected $compress = false;
/**
* Strict ON flag
*
* Whether we're running in strict SQL mode.
*
* @var boolean
*/
protected $strictOn;
/**
* Settings for a failover connection.
*
* @var array
*/
protected $failover = [];
//--------------------------------------------------------------------
/**
* The last query object that was executed
* on this connection.
*
* @var array
*/
protected $lastQuery;
/**
* Connection ID
*
* @var object|resource
*/
public $connID = false;
/**
* Result ID
*
* @var object|resource
*/
public $resultID = false;
/**
* Protect identifiers flag
*
* @var boolean
*/
public $protectIdentifiers = true;
/**
* List of reserved identifiers
*
* Identifiers that must NOT be escaped.
*
* @var array
*/
protected $reservedIdentifiers = ['*'];
/**
* Identifier escape character
*
* @var string
*/
public $escapeChar = '"';
/**
* ESCAPE statement string
*
* @var string
*/
public $likeEscapeStr = " ESCAPE '%s' ";
/**
* ESCAPE character
*
* @var string
*/
public $likeEscapeChar = '!';
/**
* Holds previously looked up data
* for performance reasons.
*
* @var array
*/
public $dataCache = [];
/**
* Microtime when connection was made
*
* @var float
*/
protected $connectTime;
/**
* How long it took to establish connection.
*
* @var float
*/
protected $connectDuration;
/**
* If true, no queries will actually be
* ran against the database.
*
* @var boolean
*/
protected $pretend = false;
/**
* Transaction enabled flag
*
* @var boolean
*/
public $transEnabled = true;
/**
* Strict transaction mode flag
*
* @var boolean
*/
public $transStrict = true;
/**
* Transaction depth level
*
* @var integer
*/
protected $transDepth = 0;
/**
* Transaction status flag
*
* Used with transactions to determine if a rollback should occur.
*
* @var boolean
*/
protected $transStatus = true;
/**
* Transaction failure flag
*
* Used with transactions to determine if a transaction has failed.
*
* @var boolean
*/
protected $transFailure = false;
/**
* Array of table aliases.
*
* @var array
*/
protected $aliasedTables = [];
//--------------------------------------------------------------------
/**
* Saves our connection settings.
*
* @param array $params
*/
public function __construct(array $params)
{
foreach ($params as $key => $value)
{
if (property_exists($this, $key))
{
$this->$key = $value;
}
}
}
//--------------------------------------------------------------------
/**
* Initializes the database connection/settings.
*
* @return mixed|void
* @throws \CodeIgniter\Database\Exceptions\DatabaseException
*/
public function initialize()
{
/* If an established connection is available, then there's
* no need to connect and select the database.
*
* Depending on the database driver, conn_id can be either
* boolean TRUE, a resource or an object.
*/
if ($this->connID)
{
return;
}
//--------------------------------------------------------------------
$this->connectTime = microtime(true);
// Connect to the database and set the connection ID
$this->connID = $this->connect($this->pConnect);
// No connection resource? Check if there is a failover else throw an error
if (! $this->connID)
{
// Check if there is a failover set
if (! empty($this->failover) && is_array($this->failover))
{
// Go over all the failovers
foreach ($this->failover as $failover)
{
// Replace the current settings with those of the failover
foreach ($failover as $key => $val)
{
if (property_exists($this, $key))
{
$this->$key = $val;
}
}
// Try to connect
$this->connID = $this->connect($this->pConnect);
// If a connection is made break the foreach loop
if ($this->connID)
{
break;
}
}
}
// We still don't have a connection?
if (! $this->connID)
{
throw new DatabaseException('Unable to connect to the database.');
}
}
$this->connectDuration = microtime(true) - $this->connectTime;
}
//--------------------------------------------------------------------
/**
* Connect to the database.
*
* @param boolean $persistent
* @return mixed
*/
abstract public function connect(bool $persistent = false);
//--------------------------------------------------------------------
/**
* Close the database connection.
*
* @return void
*/
public function close()
{
if ($this->connID)
{
$this->_close();
$this->connID = false;
}
}
//--------------------------------------------------------------------
/**
* Platform dependent way method for closing the connection.
*
* @return mixed
*/
abstract protected function _close();
//--------------------------------------------------------------------
/**
* Create a persistent database connection.
*
* @return mixed
*/
public function persistentConnect()
{
return $this->connect(true);
}
//--------------------------------------------------------------------
/**
* Keep or establish the connection if no queries have been sent for
* a length of time exceeding the server's idle timeout.
*
* @return mixed
*/
abstract public function reconnect();
//--------------------------------------------------------------------
/**
* Returns the actual connection object. If both a 'read' and 'write'
* connection has been specified, you can pass either term in to
* get that connection. If you pass either alias in and only a single
* connection is present, it must return the sole connection.
*
* @param string|null $alias
*
* @return mixed
*/
public function getConnection(string $alias = null)
{
//@todo work with read/write connections
return $this->connID;
}
//--------------------------------------------------------------------
/**
* Select a specific database table to use.
*
* @param string $databaseName
*
* @return mixed
*/
abstract public function setDatabase(string $databaseName);
//--------------------------------------------------------------------
/**
* Returns the name of the current database being used.
*
* @return string
*/
public function getDatabase(): string
{
return empty($this->database) ? '' : $this->database;
}
//--------------------------------------------------------------------
/**
* Set DB Prefix
*
* Set's the DB Prefix to something new without needing to reconnect
*
* @param string $prefix The prefix
*
* @return string
*/
public function setPrefix(string $prefix = ''): string
{
return $this->DBPrefix = $prefix;
}
//--------------------------------------------------------------------
/**
* Returns the database prefix.
*
* @return string
*/
public function getPrefix(): string
{
return $this->DBPrefix;
}
//--------------------------------------------------------------------
/**
* Returns the last error encountered by this connection.
*
* @return mixed
*/
public function getError()
{
}
//--------------------------------------------------------------------
/**
* The name of the platform in use (MySQLi, mssql, etc)
*
* @return string
*/
public function getPlatform(): string
{
return $this->DBDriver;
}
//--------------------------------------------------------------------
/**
* Returns a string containing the version of the database being used.
*
* @return string
*/
abstract public function getVersion(): string;
//--------------------------------------------------------------------
/**
* Sets the Table Aliases to use. These are typically
* collected during use of the Builder, and set here
* so queries are built correctly.
*
* @param array $aliases
*
* @return $this
*/
public function setAliasedTables(array $aliases)
{
$this->aliasedTables = $aliases;
return $this;
}
//--------------------------------------------------------------------
/**
* Add a table alias to our list.
*
* @param string $table
*
* @return $this
*/
public function addTableAlias(string $table)
{
if (! in_array($table, $this->aliasedTables))
{
$this->aliasedTables[] = $table;
}
return $this;
}
/**
* Executes the query against the database.
*
* @param $sql
*
* @return mixed
*/
abstract protected function execute(string $sql);
//--------------------------------------------------------------------
/**
* Orchestrates a query against the database. Queries must use
* Database\Statement objects to store the query and build it.
* This method works with the cache.
*
* Should automatically handle different connections for read/write
* queries if needed.
*
* @param string $sql
* @param mixed ...$binds
* @param boolean $setEscapeFlags
* @param string $queryClass
*
* @return BaseResult|Query|false
*/
public function query(string $sql, $binds = null, bool $setEscapeFlags = true, string $queryClass = 'CodeIgniter\\Database\\Query')
{
if (empty($this->connID))
{
$this->initialize();
}
$resultClass = str_replace('Connection', 'Result', get_class($this));
/**
* @var Query $query
*/
$query = new $queryClass($this);
$query->setQuery($sql, $binds, $setEscapeFlags);
if (! empty($this->swapPre) && ! empty($this->DBPrefix))
{
$query->swapPrefix($this->DBPrefix, $this->swapPre);
}
$startTime = microtime(true);
// Always save the last query so we can use
// the getLastQuery() method.
$this->lastQuery = $query;
// Run the query for real
if (! $this->pretend && false === ($this->resultID = $this->simpleQuery($query->getQuery())))
{
$query->setDuration($startTime, $startTime);
// This will trigger a rollback if transactions are being used
if ($this->transDepth !== 0)
{
$this->transStatus = false;
}
if ($this->DBDebug)
{
// We call this function in order to roll-back queries
// if transactions are enabled. If we don't call this here
// the error message will trigger an exit, causing the
// transactions to remain in limbo.
while ($this->transDepth !== 0)
{
$transDepth = $this->transDepth;
$this->transComplete();
if ($transDepth === $this->transDepth)
{
log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
break;
}
}
return false;
}
if (! $this->pretend)
{
// Let others do something with this query.
Events::trigger('DBQuery', $query);
}
return new $resultClass($this->connID, $this->resultID);
}
$query->setDuration($startTime);
if (! $this->pretend)
{
// Let others do something with this query
Events::trigger('DBQuery', $query);
}
// If $pretend is true, then we just want to return
// the actual query object here. There won't be
// any results to return.
return $this->pretend ? $query : new $resultClass($this->connID, $this->resultID);
}
//--------------------------------------------------------------------
/**
* Performs a basic query against the database. No binding or caching
* is performed, nor are transactions handled. Simply takes a raw
* query string and returns the database-specific result id.
*
* @param string $sql
*
* @return mixed
*/
public function simpleQuery(string $sql)
{
if (empty($this->connID))
{
$this->initialize();
}
return $this->execute($sql);
}
//--------------------------------------------------------------------
/**
* Disable Transactions
*
* This permits transactions to be disabled at run-time.
*
* @return void
*/
public function transOff()
{
$this->transEnabled = false;
}
//--------------------------------------------------------------------
/**
* Enable/disable Transaction Strict Mode
*
* When strict mode is enabled, if you are running multiple groups of
* transactions, if one group fails all subsequent groups will be
* rolled back.
*
* If strict mode is disabled, each group is treated autonomously,
* meaning a failure of one group will not affect any others
*
* @param boolean $mode = true
*
* @return $this
*/
public function transStrict(bool $mode = true)
{
$this->transStrict = $mode;
return $this;
}
//--------------------------------------------------------------------
/**
* Start Transaction
*
* @param boolean $test_mode = FALSE
* @return boolean
*/
public function transStart(bool $test_mode = false): bool
{
if (! $this->transEnabled)
{
return false;
}
return $this->transBegin($test_mode);
}
//--------------------------------------------------------------------
/**
* Complete Transaction
*
* @return boolean
*/
public function transComplete(): bool
{
if (! $this->transEnabled)
{
return false;
}
// The query() function will set this flag to FALSE in the event that a query failed
if ($this->transStatus === false || $this->transFailure === true)
{
$this->transRollback();
// If we are NOT running in strict mode, we will reset
// the _trans_status flag so that subsequent groups of
// transactions will be permitted.
if ($this->transStrict === false)
{
$this->transStatus = true;
}
// log_message('debug', 'DB Transaction Failure');
return false;
}
return $this->transCommit();
}
//--------------------------------------------------------------------
/**
* Lets you retrieve the transaction flag to determine if it has failed
*
* @return boolean
*/
public function transStatus(): bool
{
return $this->transStatus;
}
//--------------------------------------------------------------------
/**
* Begin Transaction
*
* @param boolean $test_mode
* @return boolean
*/
public function transBegin(bool $test_mode = false): bool
{
if (! $this->transEnabled)
{
return false;
}
// When transactions are nested we only begin/commit/rollback the outermost ones
elseif ($this->transDepth > 0)
{
$this->transDepth ++;
return true;
}
if (empty($this->connID))
{
$this->initialize();
}
// Reset the transaction failure flag.
// If the $test_mode flag is set to TRUE transactions will be rolled back
// even if the queries produce a successful result.
$this->transFailure = ($test_mode === true);
if ($this->_transBegin())
{
$this->transDepth ++;
return true;
}
return false;
}
//--------------------------------------------------------------------
/**
* Commit Transaction
*
* @return boolean
*/
public function transCommit(): bool
{
if (! $this->transEnabled || $this->transDepth === 0)
{
return false;
}
// When transactions are nested we only begin/commit/rollback the outermost ones
elseif ($this->transDepth > 1 || $this->_transCommit())
{
$this->transDepth --;
return true;
}
return false;
}
//--------------------------------------------------------------------
/**
* Rollback Transaction
*
* @return boolean
*/
public function transRollback(): bool
{
if (! $this->transEnabled || $this->transDepth === 0)
{
return false;
}
// When transactions are nested we only begin/commit/rollback the outermost ones
elseif ($this->transDepth > 1 || $this->_transRollback())
{
$this->transDepth --;
return true;
}
return false;
}
//--------------------------------------------------------------------
/**
* Begin Transaction
*
* @return boolean
*/
abstract protected function _transBegin(): bool;
//--------------------------------------------------------------------
/**
* Commit Transaction
*
* @return boolean
*/
abstract protected function _transCommit(): bool;
//--------------------------------------------------------------------
/**
* Rollback Transaction
*
* @return boolean
*/
abstract protected function _transRollback(): bool;
//--------------------------------------------------------------------
/**
* Returns an instance of the query builder for this connection.
*
* @param string|array $tableName
*
* @return BaseBuilder
* @throws DatabaseException
*/
public function table($tableName)
{
if (empty($tableName))
{
throw new DatabaseException('You must set the database table to be used with your query.');
}
$className = str_replace('Connection', 'Builder', get_class($this));
return new $className($tableName, $this);
}
//--------------------------------------------------------------------
/**
* Creates a prepared statement with the database that can then
* be used to execute multiple statements against. Within the
* closure, you would build the query in any normal way, though
* the Query Builder is the expected manner.
*
* Example:
* $stmt = $db->prepare(function($db)
* {
* return $db->table('users')
* ->where('id', 1)
* ->get();
* })
*
* @param \Closure $func
* @param array $options Passed to the prepare() method
*
* @return BasePreparedQuery|null
*/
public function prepare(\Closure $func, array $options = [])
{
if (empty($this->connID))
{
$this->initialize();
}
$this->pretend(true);
$sql = $func($this);
$this->pretend(false);
if ($sql instanceof QueryInterface)
{
$sql = $sql->getOriginalQuery();
}
$class = str_ireplace('Connection', 'PreparedQuery', get_class($this));
/**
* @var BasePreparedQuery $class
*/
$class = new $class($this);
return $class->prepare($sql, $options);
}
//--------------------------------------------------------------------
/**
* Returns the last query's statement object.
*
* @return mixed
*/
public function getLastQuery()
{
return $this->lastQuery;
}
//--------------------------------------------------------------------
/**
* Returns a string representation of the last query's statement object.
*
* @return string
*/
public function showLastQuery(): string
{
return (string) $this->lastQuery;
}
//--------------------------------------------------------------------
/**
* Returns the time we started to connect to this database in
* seconds with microseconds.
*
* Used by the Debug Toolbar's timeline.
*
* @return float
*/
public function getConnectStart(): float
{
return $this->connectTime;
}
//--------------------------------------------------------------------
/**
* Returns the number of seconds with microseconds that it took
* to connect to the database.
*
* Used by the Debug Toolbar's timeline.
*
* @param integer $decimals
*
* @return string
*/
public function getConnectDuration(int $decimals = 6): string
{
return number_format($this->connectDuration, $decimals);
}
//--------------------------------------------------------------------
/**
* Protect Identifiers
*
* This function is used extensively by the Query Builder class, and by
* a couple functions in this class.
* It takes a column or table name (optionally with an alias) and inserts
* the table prefix onto it. Some logic is necessary in order to deal with
* column names that include the path. Consider a query like this:
*
* SELECT hostname.database.table.column AS c FROM hostname.database.table
*
* Or a query with aliasing:
*
* SELECT m.member_id, m.member_name FROM members AS m
*
* Since the column name can include up to four segments (host, DB, table, column)
* or also have an alias prefix, we need to do a bit of work to figure this out and
* insert the table prefix (if it exists) in the proper position, and escape only
* the correct identifiers.
*
* @param string|array $item
* @param boolean $prefixSingle
* @param boolean $protectIdentifiers
* @param boolean $fieldExists
*
* @return string|array
*/
public function protectIdentifiers($item, bool $prefixSingle = false, bool $protectIdentifiers = null, bool $fieldExists = true)
{
if (! is_bool($protectIdentifiers))
{
$protectIdentifiers = $this->protectIdentifiers;
}
if (is_array($item))
{
$escaped_array = [];
foreach ($item as $k => $v)
{
$escaped_array[$this->protectIdentifiers($k)] = $this->protectIdentifiers($v, $prefixSingle, $protectIdentifiers, $fieldExists);
}
return $escaped_array;
}
// This is basically a bug fix for queries that use MAX, MIN, etc.
// If a parenthesis is found we know that we do not need to
// escape the data or add a prefix. There's probably a more graceful
// way to deal with this, but I'm not thinking of it
//
// Added exception for single quotes as well, we don't want to alter
// literal strings.
if (strcspn($item, "()'") !== strlen($item))
{
return $item;
}
// Convert tabs or multiple spaces into single spaces
$item = preg_replace('/\s+/', ' ', trim($item));
// If the item has an alias declaration we remove it and set it aside.
// Note: strripos() is used in order to support spaces in table names
if ($offset = strripos($item, ' AS '))
{
$alias = ($protectIdentifiers) ? substr($item, $offset, 4) . $this->escapeIdentifiers(substr($item, $offset + 4)) : substr($item, $offset);
$item = substr($item, 0, $offset);
}
elseif ($offset = strrpos($item, ' '))
{
$alias = ($protectIdentifiers) ? ' ' . $this->escapeIdentifiers(substr($item, $offset + 1)) : substr($item, $offset);
$item = substr($item, 0, $offset);
}
else
{
$alias = '';
}
// Break the string apart if it contains periods, then insert the table prefix
// in the correct location, assuming the period doesn't indicate that we're dealing
// with an alias. While we're at it, we will escape the components
if (strpos($item, '.') !== false)
{
$parts = explode('.', $item);
// Does the first segment of the exploded item match
// one of the aliases previously identified? If so,
// we have nothing more to do other than escape the item
//
// NOTE: The ! empty() condition prevents this method
// from breaking when QB isn't enabled.
if (! empty($this->aliasedTables) && in_array($parts[0], $this->aliasedTables))
{
if ($protectIdentifiers === true)
{
foreach ($parts as $key => $val)
{
if (! in_array($val, $this->reservedIdentifiers))
{
$parts[$key] = $this->escapeIdentifiers($val);
}
}
$item = implode('.', $parts);
}
return $item . $alias;
}
// Is there a table prefix defined in the config file? If not, no need to do anything
if ($this->DBPrefix !== '')
{
// We now add the table prefix based on some logic.
// Do we have 4 segments (hostname.database.table.column)?
// If so, we add the table prefix to the column name in the 3rd segment.
if (isset($parts[3]))
{
$i = 2;
}
// Do we have 3 segments (database.table.column)?
// If so, we add the table prefix to the column name in 2nd position
elseif (isset($parts[2]))
{
$i = 1;
}
// Do we have 2 segments (table.column)?
// If so, we add the table prefix to the column name in 1st segment
else
{
$i = 0;
}
// This flag is set when the supplied $item does not contain a field name.
// This can happen when this function is being called from a JOIN.
if ($fieldExists === false)
{
$i ++;
}
// Verify table prefix and replace if necessary
if ($this->swapPre !== '' && strpos($parts[$i], $this->swapPre) === 0)
{
$parts[$i] = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $parts[$i]);
}
// We only add the table prefix if it does not already exist
elseif (strpos($parts[$i], $this->DBPrefix) !== 0)
{
$parts[$i] = $this->DBPrefix . $parts[$i];
}
// Put the parts back together
$item = implode('.', $parts);
}
if ($protectIdentifiers === true)
{
$item = $this->escapeIdentifiers($item);
}
return $item . $alias;
}
// In some cases, especially 'from', we end up running through
// protect_identifiers twice. This algorithm won't work when
// it contains the escapeChar so strip it out.
$item = trim($item, $this->escapeChar);
// Is there a table prefix? If not, no need to insert it
if ($this->DBPrefix !== '')
{
// Verify table prefix and replace if necessary
if ($this->swapPre !== '' && strpos($item, $this->swapPre) === 0)
{
$item = preg_replace('/^' . $this->swapPre . '(\S+?)/', $this->DBPrefix . '\\1', $item);
}
// Do we prefix an item with no segments?
elseif ($prefixSingle === true && strpos($item, $this->DBPrefix) !== 0)
{
$item = $this->DBPrefix . $item;
}
}
if ($protectIdentifiers === true && ! in_array($item, $this->reservedIdentifiers))
{
$item = $this->escapeIdentifiers($item);
}
return $item . $alias;
}
//--------------------------------------------------------------------
/**
* Escape the SQL Identifiers
*
* This function escapes column and table names
*
* @param mixed $item
*
* @return mixed
*/
public function escapeIdentifiers($item)
{
if ($this->escapeChar === '' || empty($item) || in_array($item, $this->reservedIdentifiers))
{
return $item;
}
elseif (is_array($item))
{
foreach ($item as $key => $value)
{
$item[$key] = $this->escapeIdentifiers($value);
}
return $item;
}
// Avoid breaking functions and literal values inside queries
elseif (ctype_digit($item) || $item[0] === "'" || ( $this->escapeChar !== '"' && $item[0] === '"') ||
strpos($item, '(') !== false
)
{
return $item;
}
static $preg_ec = [];
if (empty($preg_ec))
{
if (is_array($this->escapeChar))
{
$preg_ec = [
preg_quote($this->escapeChar[0], '/'),
preg_quote($this->escapeChar[1], '/'),
$this->escapeChar[0],
$this->escapeChar[1],
];
}
else
{
$preg_ec[0] = $preg_ec[1] = preg_quote($this->escapeChar, '/');
$preg_ec[2] = $preg_ec[3] = $this->escapeChar;
}
}
foreach ($this->reservedIdentifiers as $id)
{
if (strpos($item, '.' . $id) !== false)
{
return preg_replace('/' . $preg_ec[0] . '?([^' . $preg_ec[1] . '\.]+)' . $preg_ec[1] . '?\./i', $preg_ec[2] . '$1' . $preg_ec[3] . '.', $item);
}
}
return preg_replace('/' . $preg_ec[0] . '?([^' . $preg_ec[1] . '\.]+)' . $preg_ec[1] . '?(\.)?/i', $preg_ec[2] . '$1' . $preg_ec[3] . '$2', $item);
}
//--------------------------------------------------------------------
/**
* DB Prefix
*
* Prepends a database prefix if one exists in configuration
*
* @param string $table the table
*
* @return string
* @throws \CodeIgniter\Database\Exceptions\DatabaseException
*/
public function prefixTable(string $table = ''): string
{
if ($table === '')
{
throw new DatabaseException('A table name is required for that operation.');
}
return $this->DBPrefix . $table;
}
//--------------------------------------------------------------------
/**
* Returns the total number of rows affected by this query.
*
* @return mixed
*/
abstract public function affectedRows(): int;
//--------------------------------------------------------------------
/**
* "Smart" Escape String
*
* Escapes data based on type.
* Sets boolean and null types
*
* @param mixed $str
*
* @return mixed
*/
public function escape($str)
{
if (is_array($str))
{
$str = array_map([&$this, 'escape'], $str);
return $str;
}
else if (is_string($str) || ( is_object($str) && method_exists($str, '__toString')))
{
return "'" . $this->escapeString($str) . "'";
}
else if (is_bool($str))
{
return ($str === false) ? 0 : 1;
}
else if (is_numeric($str) && $str < 0)
{
return "'{$str}'";
}
else if ($str === null)
{
return 'NULL';
}
return $str;
}
//--------------------------------------------------------------------
/**
* Escape String
*
* @param string|string[] $str Input string
* @param boolean $like Whether or not the string will be used in a LIKE condition
* @return string|string[]
*/
public function escapeString($str, bool $like = false)
{
if (is_array($str))
{
foreach ($str as $key => $val)
{
$str[$key] = $this->escapeString($val, $like);
}
return $str;
}
$str = $this->_escapeString($str);
// escape LIKE condition wildcards
if ($like === true)
{
return str_replace([
$this->likeEscapeChar,
'%',
'_',
], [
$this->likeEscapeChar . $this->likeEscapeChar,
$this->likeEscapeChar . '%',
$this->likeEscapeChar . '_',
], $str
);
}
return $str;
}
//--------------------------------------------------------------------
/**
* Escape LIKE String
*
* Calls the individual driver for platform
* specific escaping for LIKE conditions
*
* @param string|string[]
* @return string|string[]
*/
public function escapeLikeString($str)
{
return $this->escapeString($str, true);
}
//--------------------------------------------------------------------
/**
* Platform independent string escape.
*
* Will likely be overridden in child classes.
*
* @param string $str
*
* @return string
*/
protected function _escapeString(string $str): string
{
return str_replace("'", "''", remove_invisible_characters($str, false));
}
//--------------------------------------------------------------------
/**
* This function enables you to call PHP database functions that are not natively included
* in CodeIgniter, in a platform independent manner.
*
* @param string $functionName
* @param array ...$params
*
* @return boolean
* @throws DatabaseException
*/
public function callFunction(string $functionName, ...$params): bool
{
$driver = ($this->DBDriver === 'postgre' ? 'pg' : strtolower($this->DBDriver)) . '_';
if (false === strpos($driver, $functionName))
{
$functionName = $driver . $functionName;
}
if (! function_exists($functionName))
{
if ($this->DBDebug)
{
throw new DatabaseException('This feature is not available for the database you are using.');
}
return false;
}
return $functionName(...$params);
}
//--------------------------------------------------------------------
//--------------------------------------------------------------------
// META Methods
//--------------------------------------------------------------------
/**
* Returns an array of table names
*
* @param boolean $constrainByPrefix = FALSE
* @return boolean|array
* @throws \CodeIgniter\Database\Exceptions\DatabaseException
*/
public function listTables(bool $constrainByPrefix = false)
{
// Is there a cached result?
if (isset($this->dataCache['table_names']) && $this->dataCache['table_names'])
{
return $constrainByPrefix ?
preg_grep("/^{$this->DBPrefix}/", $this->dataCache['table_names'])
: $this->dataCache['table_names'];
}
if (false === ($sql = $this->_listTables($constrainByPrefix)))
{
if ($this->DBDebug)
{
throw new DatabaseException('This feature is not available for the database you are using.');
}
return false;
}
$this->dataCache['table_names'] = [];
$query = $this->query($sql);
foreach ($query->getResultArray() as $row)
{
// Do we know from which column to get the table name?
if (! isset($key))
{
if (isset($row['table_name']))
{
$key = 'table_name';
}
elseif (isset($row['TABLE_NAME']))
{
$key = 'TABLE_NAME';
}
else
{
/* We have no other choice but to just get the first element's key.
* Due to array_shift() accepting its argument by reference, if
* E_STRICT is on, this would trigger a warning. So we'll have to
* assign it first.
*/
$key = array_keys($row);
$key = array_shift($key);
}
}
$this->dataCache['table_names'][] = $row[$key];
}
return $this->dataCache['table_names'];
}
//--------------------------------------------------------------------
/**
* Determine if a particular table exists
*
* @param string $tableName
* @return boolean
*/
public function tableExists(string $tableName): bool
{
return in_array($this->protectIdentifiers($tableName, true, false, false), $this->listTables());
}
//--------------------------------------------------------------------
/**
* Fetch Field Names
*
* @param string $table Table name
*
* @return array|false
* @throws DatabaseException
*/
public function getFieldNames(string $table)
{
// Is there a cached result?
if (isset($this->dataCache['field_names'][$table]))
{
return $this->dataCache['field_names'][$table];
}
if (empty($this->connID))
{
$this->initialize();
}
if (false === ($sql = $this->_listColumns($table)))
{
if ($this->DBDebug)
{
throw new DatabaseException('This feature is not available for the database you are using.');
}
return false;
}
$query = $this->query($sql);
$this->dataCache['field_names'][$table] = [];
foreach ($query->getResultArray() as $row)
{
// Do we know from where to get the column's name?
if (! isset($key))
{
if (isset($row['column_name']))
{
$key = 'column_name';
}
elseif (isset($row['COLUMN_NAME']))
{
$key = 'COLUMN_NAME';
}
else
{
// We have no other choice but to just get the first element's key.
$key = key($row);
}
}
$this->dataCache['field_names'][$table][] = $row[$key];
}
return $this->dataCache['field_names'][$table];
}
//--------------------------------------------------------------------
/**
* Determine if a particular field exists
*
* @param string $fieldName
* @param string $tableName
* @return boolean
*/
public function fieldExists(string $fieldName, string $tableName): bool
{
return in_array($fieldName, $this->getFieldNames($tableName));
}
//--------------------------------------------------------------------
/**
* Returns an object with field data
*
* @param string $table the table name
* @return array|false
*/
public function getFieldData(string $table)
{
$fields = $this->_fieldData($this->protectIdentifiers($table, true, false, false));
return $fields ?? false;
}
//--------------------------------------------------------------------
/**
* Returns an object with key data
*
* @param string $table the table name
* @return array|false
*/
public function getIndexData(string $table)
{
$fields = $this->_indexData($this->protectIdentifiers($table, true, false, false));
return $fields ?? false;
}
//--------------------------------------------------------------------
/**
* Returns an object with foreign key data
*
* @param string $table the table name
* @return array|false
*/
public function getForeignKeyData(string $table)
{
$fields = $this->_foreignKeyData($this->protectIdentifiers($table, true, false, false));
return $fields ?? false;
}
//--------------------------------------------------------------------
/**
* Disables foreign key checks temporarily.
*/
public function disableForeignKeyChecks()
{
$sql = $this->_disableForeignKeyChecks();
return $this->query($sql);
}
//--------------------------------------------------------------------
/**
* Enables foreign key checks temporarily.
*/
public function enableForeignKeyChecks()
{
$sql = $this->_enableForeignKeyChecks();
return $this->query($sql);
}
//--------------------------------------------------------------------
/**
* Allows the engine to be set into a mode where queries are not
* actually executed, but they are still generated, timed, etc.
*
* This is primarily used by the prepared query functionality.
*
* @param boolean $pretend
*
* @return $this
*/
public function pretend(bool $pretend = true)
{
$this->pretend = $pretend;
return $this;
}
//--------------------------------------------------------------------
/**
* Empties our data cache. Especially helpful during testing.
*
* @return $this
*/
public function resetDataCache()
{
$this->dataCache = [];
return $this;
}
//--------------------------------------------------------------------
/**
* Returns the last error code and message.
*
* Must return an array with keys 'code' and 'message':
*
* return ['code' => null, 'message' => null);
*
* @return array
*/
abstract public function error(): array;
//--------------------------------------------------------------------
/**
* Insert ID
*
* @return integer
*/
abstract public function insertID(): int;
//--------------------------------------------------------------------
/**
* Generates the SQL for listing tables in a platform-dependent manner.
*
* @param boolean $constrainByPrefix
*
* @return string
*/
abstract protected function _listTables(bool $constrainByPrefix = false): string;
//--------------------------------------------------------------------
/**
* Generates a platform-specific query string so that the column names can be fetched.
*
* @param string $table
*
* @return string
*/
abstract protected function _listColumns(string $table = ''): string;
//--------------------------------------------------------------------
/**
* Platform-specific field data information.
*
* @param string $table
* @see getFieldData()
* @return array
*/
abstract protected function _fieldData(string $table): array;
//--------------------------------------------------------------------
/**
* Platform-specific index data.
*
* @param string $table
* @see getIndexData()
* @return array
*/
abstract protected function _indexData(string $table): array;
//--------------------------------------------------------------------
/**
* Platform-specific foreign keys data.
*
* @param string $table
* @see getForeignKeyData()
* @return array
*/
abstract protected function _foreignKeyData(string $table): array;
//--------------------------------------------------------------------
/**
* Accessor for properties if they exist.
*
* @param string $key
*
* @return mixed
*/
public function __get(string $key)
{
if (property_exists($this, $key))
{
return $this->$key;
}
return null;
}
//--------------------------------------------------------------------
/**
* Checker for properties existence.
*
* @param string $key
*
* @return boolean
*/
public function __isset(string $key): bool
{
return property_exists($this, $key);
}
//--------------------------------------------------------------------
}