This commit is contained in:
Markus
2022-04-28 09:40:10 +02:00
commit 795794f992
9586 changed files with 1146991 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use BadMethodCallException;
use CodeIgniter\Events\Events;
/**
* Base prepared query
*/
abstract class BasePreparedQuery implements PreparedQueryInterface
{
/**
* The prepared statement itself.
*
* @var object|resource
*/
protected $statement;
/**
* The error code, if any.
*
* @var int
*/
protected $errorCode;
/**
* The error message, if any.
*
* @var string
*/
protected $errorString;
/**
* Holds the prepared query object
* that is cloned during execute.
*
* @var Query
*/
protected $query;
/**
* A reference to the db connection to use.
*
* @var BaseConnection
*/
protected $db;
public function __construct(BaseConnection $db)
{
$this->db = &$db;
}
/**
* Prepares the query against the database, and saves the connection
* info necessary to execute the query later.
*
* NOTE: This version is based on SQL code. Child classes should
* override this method.
*
* @return mixed
*/
public function prepare(string $sql, array $options = [], string $queryClass = 'CodeIgniter\\Database\\Query')
{
// We only supports positional placeholders (?)
// in order to work with the execute method below, so we
// need to replace our named placeholders (:name)
$sql = preg_replace('/:[^\s,)]+/', '?', $sql);
/** @var Query $query */
$query = new $queryClass($this->db);
$query->setQuery($sql);
if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix)) {
$query->swapPrefix($this->db->DBPrefix, $this->db->swapPre);
}
$this->query = $query;
return $this->_prepare($query->getOriginalQuery(), $options);
}
/**
* The database-dependent portion of the prepare statement.
*
* @return mixed
*/
abstract public function _prepare(string $sql, array $options = []);
/**
* Takes a new set of data and runs it against the currently
* prepared query. Upon success, will return a Results object.
*
* @return ResultInterface
*/
public function execute(...$data)
{
// Execute the Query.
$startTime = microtime(true);
$this->_execute($data);
// Update our query object
$query = clone $this->query;
$query->setBinds($data);
$query->setDuration($startTime);
// Let others do something with this query
Events::trigger('DBQuery', $query);
// Return a result object
$resultClass = str_replace('PreparedQuery', 'Result', static::class);
$resultID = $this->_getResult();
return new $resultClass($this->db->connID, $resultID);
}
/**
* The database dependant version of the execute method.
*/
abstract public function _execute(array $data): bool;
/**
* Returns the result object for the prepared query.
*
* @return mixed
*/
abstract public function _getResult();
/**
* Explicitly closes the statement.
*/
public function close()
{
if (! is_object($this->statement) || ! method_exists($this->statement, 'close')) {
return;
}
$this->statement->close();
}
/**
* Returns the SQL that has been prepared.
*/
public function getQueryString(): string
{
if (! $this->query instanceof QueryInterface) {
throw new BadMethodCallException('Cannot call getQueryString on a prepared query until after the query has been prepared.');
}
return $this->query->getQuery();
}
/**
* A helper to determine if any error exists.
*/
public function hasError(): bool
{
return ! empty($this->errorString);
}
/**
* Returns the error code created while executing this statement.
*/
public function getErrorCode(): int
{
return $this->errorCode;
}
/**
* Returns the error message created while executing this statement.
*/
public function getErrorMessage(): string
{
return $this->errorString;
}
}

View File

@@ -0,0 +1,512 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Entity\Entity;
/**
* Class BaseResult
*/
abstract class BaseResult implements ResultInterface
{
/**
* Connection ID
*
* @var object|resource
*/
public $connID;
/**
* Result ID
*
* @var false|object|resource
*/
public $resultID;
/**
* Result Array
*
* @var array[]
*/
public $resultArray = [];
/**
* Result Object
*
* @var object[]
*/
public $resultObject = [];
/**
* Custom Result Object
*
* @var array
*/
public $customResultObject = [];
/**
* Current Row index
*
* @var int
*/
public $currentRow = 0;
/**
* The number of records in the query result
*
* @var int|null
*/
protected $numRows;
/**
* Row data
*
* @var array|null
*/
public $rowData;
/**
* Constructor
*
* @param object|resource $connID
* @param object|resource $resultID
*/
public function __construct(&$connID, &$resultID)
{
$this->connID = $connID;
$this->resultID = $resultID;
}
/**
* Retrieve the results of the query. Typically an array of
* individual data rows, which can be either an 'array', an
* 'object', or a custom class name.
*
* @param string $type The row type. Either 'array', 'object', or a class name to use
*/
public function getResult(string $type = 'object'): array
{
if ($type === 'array') {
return $this->getResultArray();
}
if ($type === 'object') {
return $this->getResultObject();
}
return $this->getCustomResultObject($type);
}
/**
* Returns the results as an array of custom objects.
*
* @return mixed
*/
public function getCustomResultObject(string $className)
{
if (isset($this->customResultObject[$className])) {
return $this->customResultObject[$className];
}
if (is_bool($this->resultID) || ! $this->resultID) {
return [];
}
// Don't fetch the result set again if we already have it
$_data = null;
if (($c = count($this->resultArray)) > 0) {
$_data = 'resultArray';
} elseif (($c = count($this->resultObject)) > 0) {
$_data = 'resultObject';
}
if ($_data !== null) {
for ($i = 0; $i < $c; $i++) {
$this->customResultObject[$className][$i] = new $className();
foreach ($this->{$_data}[$i] as $key => $value) {
$this->customResultObject[$className][$i]->{$key} = $value;
}
}
return $this->customResultObject[$className];
}
if ($this->rowData !== null) {
$this->dataSeek();
}
$this->customResultObject[$className] = [];
while ($row = $this->fetchObject($className)) {
if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) {
$row->syncOriginal();
}
$this->customResultObject[$className][] = $row;
}
return $this->customResultObject[$className];
}
/**
* Returns the results as an array of arrays.
*
* If no results, an empty array is returned.
*/
public function getResultArray(): array
{
if (! empty($this->resultArray)) {
return $this->resultArray;
}
// In the event that query caching is on, the result_id variable
// will not be a valid resource so we'll simply return an empty
// array.
if (is_bool($this->resultID) || ! $this->resultID) {
return [];
}
if ($this->resultObject) {
foreach ($this->resultObject as $row) {
$this->resultArray[] = (array) $row;
}
return $this->resultArray;
}
if ($this->rowData !== null) {
$this->dataSeek();
}
while ($row = $this->fetchAssoc()) {
$this->resultArray[] = $row;
}
return $this->resultArray;
}
/**
* Returns the results as an array of objects.
*
* If no results, an empty array is returned.
*/
public function getResultObject(): array
{
if (! empty($this->resultObject)) {
return $this->resultObject;
}
// In the event that query caching is on, the result_id variable
// will not be a valid resource so we'll simply return an empty
// array.
if (is_bool($this->resultID) || ! $this->resultID) {
return [];
}
if ($this->resultArray) {
foreach ($this->resultArray as $row) {
$this->resultObject[] = (object) $row;
}
return $this->resultObject;
}
if ($this->rowData !== null) {
$this->dataSeek();
}
while ($row = $this->fetchObject()) {
if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) {
$row->syncOriginal();
}
$this->resultObject[] = $row;
}
return $this->resultObject;
}
/**
* Wrapper object to return a row as either an array, an object, or
* a custom class.
*
* If row doesn't exist, returns null.
*
* @param mixed $n The index of the results to return
* @param string $type The type of result object. 'array', 'object' or class name.
*
* @return mixed
*/
public function getRow($n = 0, string $type = 'object')
{
if (! is_numeric($n)) {
// We cache the row data for subsequent uses
if (! is_array($this->rowData)) {
$this->rowData = $this->getRowArray();
}
// array_key_exists() instead of isset() to allow for NULL values
if (empty($this->rowData) || ! array_key_exists($n, $this->rowData)) {
return null;
}
return $this->rowData[$n];
}
if ($type === 'object') {
return $this->getRowObject($n);
}
if ($type === 'array') {
return $this->getRowArray($n);
}
return $this->getCustomRowObject($n, $type);
}
/**
* Returns a row as a custom class instance.
*
* If row doesn't exists, returns null.
*
* @return mixed
*/
public function getCustomRowObject(int $n, string $className)
{
if (! isset($this->customResultObject[$className])) {
$this->getCustomResultObject($className);
}
if (empty($this->customResultObject[$className])) {
return null;
}
if ($n !== $this->currentRow && isset($this->customResultObject[$className][$n])) {
$this->currentRow = $n;
}
return $this->customResultObject[$className][$this->currentRow];
}
/**
* Returns a single row from the results as an array.
*
* If row doesn't exist, returns null.
*
* @return mixed
*/
public function getRowArray(int $n = 0)
{
$result = $this->getResultArray();
if (empty($result)) {
return null;
}
if ($n !== $this->currentRow && isset($result[$n])) {
$this->currentRow = $n;
}
return $result[$this->currentRow];
}
/**
* Returns a single row from the results as an object.
*
* If row doesn't exist, returns null.
*
* @return mixed
*/
public function getRowObject(int $n = 0)
{
$result = $this->getResultObject();
if (empty($result)) {
return null;
}
if ($n !== $this->customResultObject && isset($result[$n])) {
$this->currentRow = $n;
}
return $result[$this->currentRow];
}
/**
* Assigns an item into a particular column slot.
*
* @param mixed $key
* @param mixed $value
*
* @return mixed
*/
public function setRow($key, $value = null)
{
// We cache the row data for subsequent uses
if (! is_array($this->rowData)) {
$this->rowData = $this->getRowArray();
}
if (is_array($key)) {
foreach ($key as $k => $v) {
$this->rowData[$k] = $v;
}
return;
}
if ($key !== '' && $value !== null) {
$this->rowData[$key] = $value;
}
}
/**
* Returns the "first" row of the current results.
*
* @return mixed
*/
public function getFirstRow(string $type = 'object')
{
$result = $this->getResult($type);
return (empty($result)) ? null : $result[0];
}
/**
* Returns the "last" row of the current results.
*
* @return mixed
*/
public function getLastRow(string $type = 'object')
{
$result = $this->getResult($type);
return (empty($result)) ? null : $result[count($result) - 1];
}
/**
* Returns the "next" row of the current results.
*
* @return mixed
*/
public function getNextRow(string $type = 'object')
{
$result = $this->getResult($type);
if (empty($result)) {
return null;
}
return isset($result[$this->currentRow + 1]) ? $result[++$this->currentRow] : null;
}
/**
* Returns the "previous" row of the current results.
*
* @return mixed
*/
public function getPreviousRow(string $type = 'object')
{
$result = $this->getResult($type);
if (empty($result)) {
return null;
}
if (isset($result[$this->currentRow - 1])) {
$this->currentRow--;
}
return $result[$this->currentRow];
}
/**
* Returns an unbuffered row and move the pointer to the next row.
*
* @return mixed
*/
public function getUnbufferedRow(string $type = 'object')
{
if ($type === 'array') {
return $this->fetchAssoc();
}
if ($type === 'object') {
return $this->fetchObject();
}
return $this->fetchObject($type);
}
/**
* Number of rows in the result set; checks for previous count, falls
* back on counting resultArray or resultObject, finally fetching resultArray
* if nothing was previously fetched
*/
public function getNumRows(): int
{
if (is_int($this->numRows)) {
return $this->numRows;
}
if ($this->resultArray !== []) {
return $this->numRows = count($this->resultArray);
}
if ($this->resultObject !== []) {
return $this->numRows = count($this->resultObject);
}
return $this->numRows = count($this->getResultArray());
}
/**
* Gets the number of fields in the result set.
*/
abstract public function getFieldCount(): int;
/**
* Generates an array of column names in the result set.
*/
abstract public function getFieldNames(): array;
/**
* Generates an array of objects representing field meta-data.
*/
abstract public function getFieldData(): array;
/**
* Frees the current result.
*/
abstract public function freeResult();
/**
* Moves the internal pointer to the desired offset. This is called
* internally before fetching results to make sure the result set
* starts at zero.
*
* @return mixed
*/
abstract public function dataSeek(int $n = 0);
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return mixed
*/
abstract protected function fetchAssoc();
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @return object
*/
abstract protected function fetchObject(string $className = 'stdClass');
}

View File

@@ -0,0 +1,321 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Class BaseUtils
*/
abstract class BaseUtils
{
/**
* Database object
*
* @var object
*/
protected $db;
/**
* List databases statement
*
* @var bool|string
*/
protected $listDatabases = false;
/**
* OPTIMIZE TABLE statement
*
* @var bool|string
*/
protected $optimizeTable = false;
/**
* REPAIR TABLE statement
*
* @var bool|string
*/
protected $repairTable = false;
/**
* Class constructor
*/
public function __construct(ConnectionInterface &$db)
{
$this->db = &$db;
}
/**
* List databases
*
* @throws DatabaseException
*
* @return array|bool
*/
public function listDatabases()
{
// Is there a cached result?
if (isset($this->db->dataCache['db_names'])) {
return $this->db->dataCache['db_names'];
}
if ($this->listDatabases === false) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
return false;
}
$this->db->dataCache['db_names'] = [];
$query = $this->db->query($this->listDatabases);
if ($query === false) {
return $this->db->dataCache['db_names'];
}
for ($i = 0, $query = $query->getResultArray(), $c = count($query); $i < $c; $i++) {
$this->db->dataCache['db_names'][] = current($query[$i]);
}
return $this->db->dataCache['db_names'];
}
/**
* Determine if a particular database exists
*/
public function databaseExists(string $databaseName): bool
{
return in_array($databaseName, $this->listDatabases(), true);
}
/**
* Optimize Table
*
* @throws DatabaseException
*
* @return bool
*/
public function optimizeTable(string $tableName)
{
if ($this->optimizeTable === false) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
return false;
}
$query = $this->db->query(sprintf($this->optimizeTable, $this->db->escapeIdentifiers($tableName)));
return $query !== false;
}
/**
* Optimize Database
*
* @throws DatabaseException
*
* @return mixed
*/
public function optimizeDatabase()
{
if ($this->optimizeTable === false) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
return false;
}
$result = [];
foreach ($this->db->listTables() as $tableName) {
$res = $this->db->query(sprintf($this->optimizeTable, $this->db->escapeIdentifiers($tableName)));
if (is_bool($res)) {
return $res;
}
// Build the result array...
$res = $res->getResultArray();
// Postgre & SQLite3 returns empty array
if (empty($res)) {
$key = $tableName;
} else {
$res = current($res);
$key = str_replace($this->db->database . '.', '', current($res));
$keys = array_keys($res);
unset($res[$keys[0]]);
}
$result[$key] = $res;
}
return $result;
}
/**
* Repair Table
*
* @throws DatabaseException
*
* @return mixed
*/
public function repairTable(string $tableName)
{
if ($this->repairTable === false) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
return false;
}
$query = $this->db->query(sprintf($this->repairTable, $this->db->escapeIdentifiers($tableName)));
if (is_bool($query)) {
return $query;
}
$query = $query->getResultArray();
return current($query);
}
/**
* Generate CSV from a query result object
*
* @return string
*/
public function getCSVFromResult(ResultInterface $query, string $delim = ',', string $newline = "\n", string $enclosure = '"')
{
$out = '';
foreach ($query->getFieldNames() as $name) {
$out .= $enclosure . str_replace($enclosure, $enclosure . $enclosure, $name) . $enclosure . $delim;
}
$out = substr($out, 0, -strlen($delim)) . $newline;
// Next blast through the result array and build out the rows
while ($row = $query->getUnbufferedRow('array')) {
$line = [];
foreach ($row as $item) {
$line[] = $enclosure . str_replace($enclosure, $enclosure . $enclosure, $item ?? '') . $enclosure;
}
$out .= implode($delim, $line) . $newline;
}
return $out;
}
/**
* Generate XML data from a query result object
*/
public function getXMLFromResult(ResultInterface $query, array $params = []): string
{
foreach (['root' => 'root', 'element' => 'element', 'newline' => "\n", 'tab' => "\t"] as $key => $val) {
if (! isset($params[$key])) {
$params[$key] = $val;
}
}
$root = $params['root'];
$newline = $params['newline'];
$tab = $params['tab'];
$element = $params['element'];
helper('xml');
$xml = '<' . $root . '>' . $newline;
while ($row = $query->getUnbufferedRow()) {
$xml .= $tab . '<' . $element . '>' . $newline;
foreach ($row as $key => $val) {
$val = (! empty($val)) ? xml_convert($val) : '';
$xml .= $tab . $tab . '<' . $key . '>' . $val . '</' . $key . '>' . $newline;
}
$xml .= $tab . '</' . $element . '>' . $newline;
}
return $xml . '</' . $root . '>' . $newline;
}
/**
* Database Backup
*
* @param array|string $params
*
* @throws DatabaseException
*
* @return mixed
*/
public function backup($params = [])
{
if (is_string($params)) {
$params = ['tables' => $params];
}
$prefs = [
'tables' => [],
'ignore' => [],
'filename' => '',
'format' => 'gzip', // gzip, txt
'add_drop' => true,
'add_insert' => true,
'newline' => "\n",
'foreign_key_checks' => true,
];
if (! empty($params)) {
foreach (array_keys($prefs) as $key) {
if (isset($params[$key])) {
$prefs[$key] = $params[$key];
}
}
}
if (empty($prefs['tables'])) {
$prefs['tables'] = $this->db->listTables();
}
if (! in_array($prefs['format'], ['gzip', 'txt'], true)) {
$prefs['format'] = 'txt';
}
if ($prefs['format'] === 'gzip' && ! function_exists('gzencode')) {
if ($this->db->DBDebug) {
throw new DatabaseException('The file compression format you chose is not supported by your server.');
}
$prefs['format'] = 'txt';
}
if ($prefs['format'] === 'txt') {
return $this->_backup($prefs);
}
return gzencode($this->_backup($prefs));
}
/**
* Platform dependent version of the backup function.
*
* @return mixed
*/
abstract public function _backup(?array $prefs = null);
}

145
system/Database/Config.php Normal file
View File

@@ -0,0 +1,145 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Config\BaseConfig;
use InvalidArgumentException;
/**
* Class Config
*/
class Config extends BaseConfig
{
/**
* Cache for instance of any connections that
* have been requested as a "shared" instance.
*
* @var array
*/
protected static $instances = [];
/**
* The main instance used to manage all of
* our open database connections.
*
* @var Database|null
*/
protected static $factory;
/**
* Creates the default
*
* @param array|string $group The name of the connection group to use, or an array of configuration settings.
* @param bool $getShared Whether to return a shared instance of the connection.
*
* @return BaseConnection
*/
public static function connect($group = null, bool $getShared = true)
{
// If a DB connection is passed in, just pass it back
if ($group instanceof BaseConnection) {
return $group;
}
if (is_array($group)) {
$config = $group;
$group = 'custom-' . md5(json_encode($config));
}
$config = $config ?? config('Database');
if (empty($group)) {
$group = ENVIRONMENT === 'testing' ? 'tests' : $config->defaultGroup;
}
if (is_string($group) && ! isset($config->{$group}) && strpos($group, 'custom-') !== 0) {
throw new InvalidArgumentException($group . ' is not a valid database connection group.');
}
if ($getShared && isset(static::$instances[$group])) {
return static::$instances[$group];
}
static::ensureFactory();
if (isset($config->{$group})) {
$config = $config->{$group};
}
$connection = static::$factory->load($config, $group);
static::$instances[$group] = &$connection;
return $connection;
}
/**
* Returns an array of all db connections currently made.
*/
public static function getConnections(): array
{
return static::$instances;
}
/**
* Loads and returns an instance of the Forge for the specified
* database group, and loads the group if it hasn't been loaded yet.
*
* @param array|ConnectionInterface|string|null $group
*
* @return Forge
*/
public static function forge($group = null)
{
$db = static::connect($group);
return static::$factory->loadForge($db);
}
/**
* Returns a new instance of the Database Utilities class.
*
* @param array|string|null $group
*
* @return BaseUtils
*/
public static function utils($group = null)
{
$db = static::connect($group);
return static::$factory->loadUtils($db);
}
/**
* Returns a new instance of the Database Seeder.
*
* @return Seeder
*/
public static function seeder(?string $group = null)
{
$config = config('Database');
return new Seeder($config, static::connect($group));
}
/**
* Ensures the database Connection Manager/Factory is loaded and ready to use.
*/
protected static function ensureFactory()
{
if (static::$factory instanceof Database) {
return;
}
static::$factory = new Database();
}
}

View File

@@ -0,0 +1,156 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
/**
* Interface ConnectionInterface
*/
interface ConnectionInterface
{
/**
* Initializes the database connection/settings.
*
* @return mixed
*/
public function initialize();
/**
* Connect to the database.
*
* @return mixed
*/
public function connect(bool $persistent = false);
/**
* Create a persistent database connection.
*
* @return mixed
*/
public function persistentConnect();
/**
* Keep or establish the connection if no queries have been sent for
* a length of time exceeding the server's idle timeout.
*
* @return mixed
*/
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.
*
* @return mixed
*/
public function getConnection(?string $alias = null);
/**
* Select a specific database table to use.
*
* @return mixed
*/
public function setDatabase(string $databaseName);
/**
* Returns the name of the current database being used.
*/
public function getDatabase(): string;
/**
* Returns the last error encountered by this connection.
* Must return this format: ['code' => string|int, 'message' => string]
* intval(code) === 0 means "no error".
*
* @return array<string, int|string>
*/
public function error(): array;
/**
* The name of the platform in use (MySQLi, mssql, etc)
*/
public function getPlatform(): string;
/**
* Returns a string containing the version of the database being used.
*/
public function getVersion(): string;
/**
* 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 mixed ...$binds
*
* @return BaseResult|bool|Query
*/
public function query(string $sql, $binds = null);
/**
* 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.
*
* @return mixed
*/
public function simpleQuery(string $sql);
/**
* Returns an instance of the query builder for this connection.
*
* @param array|string $tableName Table name.
*
* @return BaseBuilder Builder.
*/
public function table($tableName);
/**
* Returns the last query's statement object.
*
* @return mixed
*/
public function getLastQuery();
/**
* "Smart" Escaping
*
* Escapes data based on type.
* Sets boolean and null types.
*
* @param mixed $str
*
* @return mixed
*/
public function escape($str);
/**
* Allows for custom calls to the database engine that are not
* supported through our database layer.
*
* @param array ...$params
*
* @return mixed
*/
public function callFunction(string $functionName, ...$params);
/**
* Determines if the statement is a write-type query or not.
*
* @param string $sql
*/
public function isWriteType($sql): bool;
}

View File

@@ -0,0 +1,138 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use InvalidArgumentException;
/**
* Database Connection Factory
*
* Creates and returns an instance of the appropriate DatabaseConnection
*/
class Database
{
/**
* Maintains an array of the instances of all connections that have
* been created.
*
* Helps to keep track of all open connections for performance
* monitoring, logging, etc.
*
* @var array
*/
protected $connections = [];
/**
* Parses the connection binds and returns an instance of the driver
* ready to go.
*
* @throws InvalidArgumentException
*
* @return mixed
*/
public function load(array $params = [], string $alias = '')
{
if ($alias === '') {
throw new InvalidArgumentException('You must supply the parameter: alias.');
}
if (! empty($params['DSN']) && strpos($params['DSN'], '://') !== false) {
$params = $this->parseDSN($params);
}
if (empty($params['DBDriver'])) {
throw new InvalidArgumentException('You have not selected a database type to connect to.');
}
$this->connections[$alias] = $this->initDriver($params['DBDriver'], 'Connection', $params);
return $this->connections[$alias];
}
/**
* Creates a Forge instance for the current database type.
*/
public function loadForge(ConnectionInterface $db): object
{
if (! $db->connID) {
$db->initialize();
}
return $this->initDriver($db->DBDriver, 'Forge', $db);
}
/**
* Creates a Utils instance for the current database type.
*/
public function loadUtils(ConnectionInterface $db): object
{
if (! $db->connID) {
$db->initialize();
}
return $this->initDriver($db->DBDriver, 'Utils', $db);
}
/**
* Parse universal DSN string
*
* @throws InvalidArgumentException
*/
protected function parseDSN(array $params): array
{
$dsn = parse_url($params['DSN']);
if (! $dsn) {
throw new InvalidArgumentException('Your DSN connection string is invalid.');
}
$dsnParams = [
'DSN' => '',
'DBDriver' => $dsn['scheme'],
'hostname' => isset($dsn['host']) ? rawurldecode($dsn['host']) : '',
'port' => isset($dsn['port']) ? rawurldecode((string) $dsn['port']) : '',
'username' => isset($dsn['user']) ? rawurldecode($dsn['user']) : '',
'password' => isset($dsn['pass']) ? rawurldecode($dsn['pass']) : '',
'database' => isset($dsn['path']) ? rawurldecode(substr($dsn['path'], 1)) : '',
];
if (! empty($dsn['query'])) {
parse_str($dsn['query'], $extra);
foreach ($extra as $key => $val) {
if (is_string($val) && in_array(strtolower($val), ['true', 'false', 'null'], true)) {
$val = $val === 'null' ? null : filter_var($val, FILTER_VALIDATE_BOOLEAN);
}
$dsnParams[$key] = $val;
}
}
return array_merge($params, $dsnParams);
}
/**
* Initialize database driver.
*
* @param array|object $argument
*/
protected function initDriver(string $driver, string $class, $argument): object
{
$class = $driver . '\\' . $class;
if (strpos($driver, '\\') === false) {
$class = "CodeIgniter\\Database\\{$class}";
}
return new $class($argument);
}
}

View File

@@ -0,0 +1,85 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Exceptions;
use CodeIgniter\Exceptions\DebugTraceableTrait;
use RuntimeException;
class DataException extends RuntimeException implements ExceptionInterface
{
use DebugTraceableTrait;
/**
* Used by the Model's trigger() method when the callback cannot be found.
*
* @return DataException
*/
public static function forInvalidMethodTriggered(string $method)
{
return new static(lang('Database.invalidEvent', [$method]));
}
/**
* Used by Model's insert/update methods when there isn't
* any data to actually work with.
*
* @return DataException
*/
public static function forEmptyDataset(string $mode)
{
return new static(lang('Database.emptyDataset', [$mode]));
}
/**
* Used by Model's insert/update methods when there is no
* primary key defined and Model has option `useAutoIncrement`
* set to false.
*
* @return DataException
*/
public static function forEmptyPrimaryKey(string $mode)
{
return new static(lang('Database.emptyPrimaryKey', [$mode]));
}
/**
* Thrown when an argument for one of the Model's methods
* were empty or otherwise invalid, and they could not be
* to work correctly for that method.
*
* @return DataException
*/
public static function forInvalidArgument(string $argument)
{
return new static(lang('Database.invalidArgument', [$argument]));
}
public static function forInvalidAllowedFields(string $model)
{
return new static(lang('Database.invalidAllowedFields', [$model]));
}
public static function forTableNotFound(string $table)
{
return new static(lang('Database.tableNotFound', [$table]));
}
public static function forEmptyInputGiven(string $argument)
{
return new static(lang('Database.forEmptyInputGiven', [$argument]));
}
public static function forFindColumnHaveMultipleColumns()
{
return new static(lang('Database.forFindColumnHaveMultipleColumns'));
}
}

View File

@@ -0,0 +1,24 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Exceptions;
use Error;
class DatabaseException extends Error implements ExceptionInterface
{
/**
* Exit status code
*
* @var int
*/
protected $code = 8;
}

View File

@@ -0,0 +1,22 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Exceptions;
/**
* Provides a domain-level interface for broad capture
* of all database-related exceptions.
*
* catch (\CodeIgniter\Database\Exceptions\ExceptionInterface) { ... }
*/
interface ExceptionInterface extends \CodeIgniter\Exceptions\ExceptionInterface
{
}

1099
system/Database/Forge.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,73 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use Config\Database;
/**
* Class Migration
*/
abstract class Migration
{
/**
* The name of the database group to use.
*
* @var string
*/
protected $DBGroup;
/**
* Database Connection instance
*
* @var ConnectionInterface
*/
protected $db;
/**
* Database Forge instance.
*
* @var Forge
*/
protected $forge;
/**
* Constructor.
*
* @param Forge $forge
*/
public function __construct(?Forge $forge = null)
{
$this->forge = $forge ?? Database::forge($this->DBGroup ?? config('Database')->defaultGroup);
$this->db = $this->forge->getConnection();
}
/**
* Returns the database group name this migration uses.
*
* @return string
*/
public function getDBGroup(): ?string
{
return $this->DBGroup;
}
/**
* Perform a migration step.
*/
abstract public function up();
/**
* Revert a migration step.
*/
abstract public function down();
}

View File

@@ -0,0 +1,867 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\CLI\CLI;
use CodeIgniter\Events\Events;
use CodeIgniter\Exceptions\ConfigException;
use Config\Database;
use Config\Migrations as MigrationsConfig;
use Config\Services;
use RuntimeException;
use stdClass;
/**
* Class MigrationRunner
*/
class MigrationRunner
{
/**
* Whether or not migrations are allowed to run.
*
* @var bool
*/
protected $enabled = false;
/**
* Name of table to store meta information
*
* @var string
*/
protected $table;
/**
* The Namespace where migrations can be found.
*
* @var string|null
*/
protected $namespace;
/**
* The database Group to migrate.
*
* @var string
*/
protected $group;
/**
* The migration name.
*
* @var string
*/
protected $name;
/**
* The pattern used to locate migration file versions.
*
* @var string
*/
protected $regex = '/^\d{4}[_-]?\d{2}[_-]?\d{2}[_-]?\d{6}_(\w+)$/';
/**
* The main database connection. Used to store
* migration information in.
*
* @var BaseConnection
*/
protected $db;
/**
* If true, will continue instead of throwing
* exceptions.
*
* @var bool
*/
protected $silent = false;
/**
* used to return messages for CLI.
*
* @var array
*/
protected $cliMessages = [];
/**
* Tracks whether we have already ensured
* the table exists or not.
*
* @var bool
*/
protected $tableChecked = false;
/**
* The full path to locate migration files.
*
* @var string
*/
protected $path;
/**
* The database Group filter.
*
* @var string|null
*/
protected $groupFilter;
/**
* Used to skip current migration.
*
* @var bool
*/
protected $groupSkip = false;
/**
* Constructor.
*
* When passing in $db, you may pass any of the following to connect:
* - group name
* - existing connection instance
* - array of database configuration values
*
* @param array|ConnectionInterface|string|null $db
*
* @throws ConfigException
*/
public function __construct(MigrationsConfig $config, $db = null)
{
$this->enabled = $config->enabled ?? false;
$this->table = $config->table ?? 'migrations';
// Default name space is the app namespace
$this->namespace = APP_NAMESPACE;
// get default database group
$config = config('Database');
$this->group = $config->defaultGroup;
unset($config);
// If no db connection passed in, use
// default database group.
$this->db = db_connect($db);
}
/**
* Locate and run all new migrations
*
* @throws ConfigException
* @throws RuntimeException
*
* @return bool
*/
public function latest(?string $group = null)
{
if (! $this->enabled) {
throw ConfigException::forDisabledMigrations();
}
$this->ensureTable();
if ($group !== null) {
$this->groupFilter = $group;
$this->setGroup($group);
}
$migrations = $this->findMigrations();
if (empty($migrations)) {
return true;
}
foreach ($this->getHistory((string) $group) as $history) {
unset($migrations[$this->getObjectUid($history)]);
}
$batch = $this->getLastBatch() + 1;
foreach ($migrations as $migration) {
if ($this->migrate('up', $migration)) {
if ($this->groupSkip === true) {
$this->groupSkip = false;
continue;
}
$this->addHistory($migration, $batch);
} else {
$this->regress(-1);
$message = lang('Migrations.generalFault');
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
}
$data = get_object_vars($this);
$data['method'] = 'latest';
Events::trigger('migrate', $data);
return true;
}
/**
* Migrate down to a previous batch
*
* Calls each migration step required to get to the provided batch
*
* @param int $targetBatch Target batch number, or negative for a relative batch, 0 for all
*
* @throws ConfigException
* @throws RuntimeException
*
* @return mixed Current batch number on success, FALSE on failure or no migrations are found
*/
public function regress(int $targetBatch = 0, ?string $group = null)
{
if (! $this->enabled) {
throw ConfigException::forDisabledMigrations();
}
// Set database group if not null
if ($group !== null) {
$this->setGroup($group);
}
$this->ensureTable();
$batches = $this->getBatches();
if ($targetBatch < 0) {
$targetBatch = $batches[count($batches) - 1 + $targetBatch] ?? 0;
}
if (empty($batches) && $targetBatch === 0) {
return true;
}
if ($targetBatch !== 0 && ! in_array($targetBatch, $batches, true)) {
$message = lang('Migrations.batchNotFound') . $targetBatch;
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
$tmpNamespace = $this->namespace;
$this->namespace = null;
$allMigrations = $this->findMigrations();
$migrations = [];
while ($batch = array_pop($batches)) {
if ($batch <= $targetBatch) {
break;
}
foreach ($this->getBatchHistory($batch, 'desc') as $history) {
$uid = $this->getObjectUid($history);
if (! isset($allMigrations[$uid])) {
$message = lang('Migrations.gap') . ' ' . $history->version;
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
$migration = $allMigrations[$uid];
$migration->history = $history;
$migrations[] = $migration;
}
}
foreach ($migrations as $migration) {
if ($this->migrate('down', $migration)) {
$this->removeHistory($migration->history);
} else {
$message = lang('Migrations.generalFault');
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
}
$data = get_object_vars($this);
$data['method'] = 'regress';
Events::trigger('migrate', $data);
$this->namespace = $tmpNamespace;
return true;
}
/**
* Migrate a single file regardless of order or batches.
* Method "up" or "down" determined by presence in history.
* NOTE: This is not recommended and provided mostly for testing.
*
* @param string $path Full path to a valid migration file
* @param string $path Namespace of the target migration
*/
public function force(string $path, string $namespace, ?string $group = null)
{
if (! $this->enabled) {
throw ConfigException::forDisabledMigrations();
}
$this->ensureTable();
if ($group !== null) {
$this->groupFilter = $group;
$this->setGroup($group);
}
$migration = $this->migrationFromFile($path, $namespace);
if (empty($migration)) {
$message = lang('Migrations.notFound');
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
$method = 'up';
$this->setNamespace($migration->namespace);
foreach ($this->getHistory($this->group) as $history) {
if ($this->getObjectUid($history) === $migration->uid) {
$method = 'down';
$migration->history = $history;
break;
}
}
if ($method === 'up') {
$batch = $this->getLastBatch() + 1;
if ($this->migrate('up', $migration) && $this->groupSkip === false) {
$this->addHistory($migration, $batch);
return true;
}
$this->groupSkip = false;
} elseif ($this->migrate('down', $migration)) {
$this->removeHistory($migration->history);
return true;
}
$message = lang('Migrations.generalFault');
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
/**
* Retrieves list of available migration scripts
*
* @return array List of all located migrations by their UID
*/
public function findMigrations(): array
{
$namespaces = $this->namespace ? [$this->namespace] : array_keys(Services::autoloader()->getNamespace());
$migrations = [];
foreach ($namespaces as $namespace) {
foreach ($this->findNamespaceMigrations($namespace) as $migration) {
$migrations[$migration->uid] = $migration;
}
}
// Sort migrations ascending by their UID (version)
ksort($migrations);
return $migrations;
}
/**
* Retrieves a list of available migration scripts for one namespace
*/
public function findNamespaceMigrations(string $namespace): array
{
$migrations = [];
$locator = Services::locator(true);
if (! empty($this->path)) {
helper('filesystem');
$dir = rtrim($this->path, DIRECTORY_SEPARATOR) . '/';
$files = get_filenames($dir, true);
} else {
$files = $locator->listNamespaceFiles($namespace, '/Database/Migrations/');
}
foreach ($files as $file) {
$file = empty($this->path) ? $file : $this->path . str_replace($this->path, '', $file);
if ($migration = $this->migrationFromFile($file, $namespace)) {
$migrations[] = $migration;
}
}
return $migrations;
}
/**
* Create a migration object from a file path.
*
* @return false|object Returns the migration object, or false on failure
*/
protected function migrationFromFile(string $path, string $namespace)
{
if (substr($path, -4) !== '.php') {
return false;
}
$name = basename($path, '.php');
if (! preg_match($this->regex, $name)) {
return false;
}
$locator = Services::locator(true);
$migration = new stdClass();
$migration->version = $this->getMigrationNumber($name);
$migration->name = $this->getMigrationName($name);
$migration->path = $path;
$migration->class = $locator->getClassname($path);
$migration->namespace = $namespace;
$migration->uid = $this->getObjectUid($migration);
return $migration;
}
/**
* Allows other scripts to modify on the fly as needed.
*
* @return MigrationRunner
*/
public function setNamespace(?string $namespace)
{
$this->namespace = $namespace;
return $this;
}
/**
* Allows other scripts to modify on the fly as needed.
*
* @return MigrationRunner
*/
public function setGroup(string $group)
{
$this->group = $group;
return $this;
}
/**
* @return MigrationRunner
*/
public function setName(string $name)
{
$this->name = $name;
return $this;
}
/**
* If $silent == true, then will not throw exceptions and will
* attempt to continue gracefully.
*
* @return MigrationRunner
*/
public function setSilent(bool $silent)
{
$this->silent = $silent;
return $this;
}
/**
* Extracts the migration number from a filename
*/
protected function getMigrationNumber(string $migration): string
{
preg_match('/^\d{4}[_-]?\d{2}[_-]?\d{2}[_-]?\d{6}/', $migration, $matches);
return count($matches) ? $matches[0] : '0';
}
/**
* Extracts the migration class name from a filename
*/
protected function getMigrationName(string $migration): string
{
$parts = explode('_', $migration);
array_shift($parts);
return implode('_', $parts);
}
/**
* Uses the non-repeatable portions of a migration or history
* to create a sortable unique key
*
* @param object $object migration or $history
*/
public function getObjectUid($object): string
{
return preg_replace('/[^0-9]/', '', $object->version) . $object->class;
}
/**
* Retrieves messages formatted for CLI output
*/
public function getCliMessages(): array
{
return $this->cliMessages;
}
/**
* Clears any CLI messages.
*
* @return MigrationRunner
*/
public function clearCliMessages()
{
$this->cliMessages = [];
return $this;
}
/**
* Truncates the history table.
*/
public function clearHistory()
{
if ($this->db->tableExists($this->table)) {
$this->db->table($this->table)->truncate();
}
}
/**
* Add a history to the table.
*
* @param object $migration
*/
protected function addHistory($migration, int $batch)
{
$this->db->table($this->table)->insert([
'version' => $migration->version,
'class' => $migration->class,
'group' => $this->group,
'namespace' => $migration->namespace,
'time' => time(),
'batch' => $batch,
]);
if (is_cli()) {
$this->cliMessages[] = sprintf(
"\t%s(%s) %s_%s",
CLI::color(lang('Migrations.added'), 'yellow'),
$migration->namespace,
$migration->version,
$migration->class
);
}
}
/**
* Removes a single history
*
* @param object $history
*/
protected function removeHistory($history)
{
$this->db->table($this->table)->where('id', $history->id)->delete();
if (is_cli()) {
$this->cliMessages[] = sprintf(
"\t%s(%s) %s_%s",
CLI::color(lang('Migrations.removed'), 'yellow'),
$history->namespace,
$history->version,
$history->class
);
}
}
/**
* Grabs the full migration history from the database for a group
*/
public function getHistory(string $group = 'default'): array
{
$this->ensureTable();
$builder = $this->db->table($this->table);
// If group was specified then use it
if (! empty($group)) {
$builder->where('group', $group);
}
// If a namespace was specified then use it
if ($this->namespace) {
$builder->where('namespace', $this->namespace);
}
$query = $builder->orderBy('id', 'ASC')->get();
return ! empty($query) ? $query->getResultObject() : [];
}
/**
* Returns the migration history for a single batch.
*
* @param string $order
*/
public function getBatchHistory(int $batch, $order = 'asc'): array
{
$this->ensureTable();
$query = $this->db->table($this->table)
->where('batch', $batch)
->orderBy('id', $order)
->get();
return ! empty($query) ? $query->getResultObject() : [];
}
/**
* Returns all the batches from the database history in order
*/
public function getBatches(): array
{
$this->ensureTable();
$batches = $this->db->table($this->table)
->select('batch')
->distinct()
->orderBy('batch', 'asc')
->get()
->getResultArray();
return array_map('intval', array_column($batches, 'batch'));
}
/**
* Returns the value of the last batch in the database.
*/
public function getLastBatch(): int
{
$this->ensureTable();
$batch = $this->db->table($this->table)
->selectMax('batch')
->get()
->getResultObject();
$batch = is_array($batch) && count($batch)
? end($batch)->batch
: 0;
return (int) $batch;
}
/**
* Returns the version number of the first migration for a batch.
* Mostly just for tests.
*/
public function getBatchStart(int $batch): string
{
if ($batch < 0) {
$batches = $this->getBatches();
$batch = $batches[count($batches) - 1] ?? 0;
}
$migration = $this->db->table($this->table)
->where('batch', $batch)
->orderBy('id', 'asc')
->limit(1)
->get()
->getResultObject();
return count($migration) ? $migration[0]->version : '0';
}
/**
* Returns the version number of the last migration for a batch.
* Mostly just for tests.
*/
public function getBatchEnd(int $batch): string
{
if ($batch < 0) {
$batches = $this->getBatches();
$batch = $batches[count($batches) - 1] ?? 0;
}
$migration = $this->db->table($this->table)
->where('batch', $batch)
->orderBy('id', 'desc')
->limit(1)
->get()
->getResultObject();
return count($migration) ? $migration[0]->version : 0;
}
/**
* Ensures that we have created our migrations table
* in the database.
*/
public function ensureTable()
{
if ($this->tableChecked || $this->db->tableExists($this->table)) {
return;
}
$forge = Database::forge($this->db);
$forge->addField([
'id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
'auto_increment' => true,
],
'version' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
],
'class' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
],
'group' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
],
'namespace' => [
'type' => 'VARCHAR',
'constraint' => 255,
'null' => false,
],
'time' => [
'type' => 'INT',
'constraint' => 11,
'null' => false,
],
'batch' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => false,
],
]);
$forge->addPrimaryKey('id');
$forge->createTable($this->table, true);
$this->tableChecked = true;
}
/**
* Handles the actual running of a migration.
*
* @param string $direction "up" or "down"
* @param object $migration The migration to run
*/
protected function migrate($direction, $migration): bool
{
include_once $migration->path;
$class = $migration->class;
$this->setName($migration->name);
// Validate the migration file structure
if (! class_exists($class, false)) {
$message = sprintf(lang('Migrations.classNotFound'), $class);
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
$instance = new $class();
$group = $instance->getDBGroup() ?? config('Database')->defaultGroup;
if (ENVIRONMENT !== 'testing' && $group === 'tests' && $this->groupFilter !== 'tests') {
// @codeCoverageIgnoreStart
$this->groupSkip = true;
return true;
// @codeCoverageIgnoreEnd
}
if ($direction === 'up' && $this->groupFilter !== null && $this->groupFilter !== $group) {
$this->groupSkip = true;
return true;
}
$this->setGroup($group);
if (! is_callable([$instance, $direction])) {
$message = sprintf(lang('Migrations.missingMethod'), $direction);
if ($this->silent) {
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
return false;
}
throw new RuntimeException($message);
}
$instance->{$direction}();
return true;
}
}

View File

@@ -0,0 +1,52 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\Config\Factories;
/**
* Returns new or shared Model instances
*
* @deprecated Use CodeIgniter\Config\Factories::models()
*
* @codeCoverageIgnore
*/
class ModelFactory
{
/**
* Creates new Model instances or returns a shared instance
*
* @return mixed
*/
public static function get(string $name, bool $getShared = true, ?ConnectionInterface $connection = null)
{
return Factories::models($name, ['getShared' => $getShared], $connection);
}
/**
* Helper method for injecting mock instances while testing.
*
* @param object $instance
*/
public static function injectMock(string $name, $instance)
{
Factories::injectMock('models', $name, $instance);
}
/**
* Resets the static arrays
*/
public static function reset()
{
Factories::reset('models');
}
}

View File

@@ -0,0 +1,56 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\BaseBuilder;
/**
* Builder for MySQLi
*/
class Builder extends BaseBuilder
{
/**
* Identifier escape character
*
* @var string
*/
protected $escapeChar = '`';
/**
* Specifies which sql statements
* support the ignore option.
*
* @var array
*/
protected $supportedIgnoreStatements = [
'update' => 'IGNORE',
'insert' => 'IGNORE',
'delete' => 'IGNORE',
];
/**
* 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.
*/
protected function _fromTables(): string
{
if (! empty($this->QBJoin) && count($this->QBFrom) > 1) {
return '(' . implode(', ', $this->QBFrom) . ')';
}
return implode(', ', $this->QBFrom);
}
}

View File

@@ -0,0 +1,605 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use LogicException;
use MySQLi;
use mysqli_sql_exception;
use stdClass;
use Throwable;
/**
* Connection for MySQLi
*/
class Connection extends BaseConnection
{
/**
* Database driver
*
* @var string
*/
public $DBDriver = 'MySQLi';
/**
* DELETE hack flag
*
* Whether to use the MySQL "delete hack" which allows the number
* of affected rows to be shown. Uses a preg_replace when enabled,
* adding a bit more processing to all queries.
*
* @var bool
*/
public $deleteHack = true;
/**
* Identifier escape character
*
* @var string
*/
public $escapeChar = '`';
/**
* MySQLi object
*
* Has to be preserved without being assigned to $conn_id.
*
* @var MySQLi
*/
public $mysqli;
/**
* MySQLi constant
*
* For unbuffered queries use `MYSQLI_USE_RESULT`.
*
* Default mode for buffered queries uses `MYSQLI_STORE_RESULT`.
*
* @var int
*/
public $resultMode = MYSQLI_STORE_RESULT;
/**
* Connect to the database.
*
* @throws DatabaseException
*
* @return mixed
*/
public function connect(bool $persistent = false)
{
// Do we have a socket path?
if ($this->hostname[0] === '/') {
$hostname = null;
$port = null;
$socket = $this->hostname;
} else {
$hostname = ($persistent === true) ? 'p:' . $this->hostname : $this->hostname;
$port = empty($this->port) ? null : $this->port;
$socket = '';
}
$clientFlags = ($this->compress === true) ? MYSQLI_CLIENT_COMPRESS : 0;
$this->mysqli = mysqli_init();
mysqli_report(MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX);
$this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10);
if (isset($this->strictOn)) {
if ($this->strictOn) {
$this->mysqli->options(
MYSQLI_INIT_COMMAND,
"SET SESSION sql_mode = CONCAT(@@sql_mode, ',', 'STRICT_ALL_TABLES')"
);
} else {
$this->mysqli->options(
MYSQLI_INIT_COMMAND,
"SET SESSION sql_mode = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
@@sql_mode,
'STRICT_ALL_TABLES,', ''),
',STRICT_ALL_TABLES', ''),
'STRICT_ALL_TABLES', ''),
'STRICT_TRANS_TABLES,', ''),
',STRICT_TRANS_TABLES', ''),
'STRICT_TRANS_TABLES', '')"
);
}
}
if (is_array($this->encrypt)) {
$ssl = [];
if (! empty($this->encrypt['ssl_key'])) {
$ssl['key'] = $this->encrypt['ssl_key'];
}
if (! empty($this->encrypt['ssl_cert'])) {
$ssl['cert'] = $this->encrypt['ssl_cert'];
}
if (! empty($this->encrypt['ssl_ca'])) {
$ssl['ca'] = $this->encrypt['ssl_ca'];
}
if (! empty($this->encrypt['ssl_capath'])) {
$ssl['capath'] = $this->encrypt['ssl_capath'];
}
if (! empty($this->encrypt['ssl_cipher'])) {
$ssl['cipher'] = $this->encrypt['ssl_cipher'];
}
if (! empty($ssl)) {
if (isset($this->encrypt['ssl_verify'])) {
if ($this->encrypt['ssl_verify']) {
if (defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT')) {
$this->mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, 1);
}
}
// Apparently (when it exists), setting MYSQLI_OPT_SSL_VERIFY_SERVER_CERT
// to FALSE didn't do anything, so PHP 5.6.16 introduced yet another
// constant ...
//
// https://secure.php.net/ChangeLog-5.php#5.6.16
// https://bugs.php.net/bug.php?id=68344
elseif (defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT') && version_compare($this->mysqli->client_info, 'mysqlnd 5.6', '>=')) {
$clientFlags += MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
}
}
$this->mysqli->ssl_set(
$ssl['key'] ?? null,
$ssl['cert'] ?? null,
$ssl['ca'] ?? null,
$ssl['capath'] ?? null,
$ssl['cipher'] ?? null
);
}
$clientFlags += MYSQLI_CLIENT_SSL;
}
try {
if ($this->mysqli->real_connect(
$hostname,
$this->username,
$this->password,
$this->database,
$port,
$socket,
$clientFlags
)) {
// Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails
if (($clientFlags & MYSQLI_CLIENT_SSL) && version_compare($this->mysqli->client_info, 'mysqlnd 5.7.3', '<=')
&& empty($this->mysqli->query("SHOW STATUS LIKE 'ssl_cipher'")->fetch_object()->Value)
) {
$this->mysqli->close();
$message = 'MySQLi was configured for an SSL connection, but got an unencrypted connection instead!';
log_message('error', $message);
if ($this->DBDebug) {
throw new DatabaseException($message);
}
return false;
}
if (! $this->mysqli->set_charset($this->charset)) {
log_message('error', "Database: Unable to set the configured connection charset ('{$this->charset}').");
$this->mysqli->close();
if ($this->DBDebug) {
throw new DatabaseException('Unable to set client connection character set: ' . $this->charset);
}
return false;
}
return $this->mysqli;
}
} catch (Throwable $e) {
// Clean sensitive information from errors.
$msg = $e->getMessage();
$msg = str_replace($this->username, '****', $msg);
$msg = str_replace($this->password, '****', $msg);
throw new DatabaseException($msg, $e->getCode(), $e);
}
return false;
}
/**
* Keep or establish the connection if no queries have been sent for
* a length of time exceeding the server's idle timeout.
*/
public function reconnect()
{
$this->close();
$this->initialize();
}
/**
* Close the database connection.
*/
protected function _close()
{
$this->connID->close();
}
/**
* Select a specific database table to use.
*/
public function setDatabase(string $databaseName): bool
{
if ($databaseName === '') {
$databaseName = $this->database;
}
if (empty($this->connID)) {
$this->initialize();
}
if ($this->connID->select_db($databaseName)) {
$this->database = $databaseName;
return true;
}
return false;
}
/**
* Returns a string containing the version of the database being used.
*/
public function getVersion(): string
{
if (isset($this->dataCache['version'])) {
return $this->dataCache['version'];
}
if (empty($this->mysqli)) {
$this->initialize();
}
return $this->dataCache['version'] = $this->mysqli->server_info;
}
/**
* Executes the query against the database.
*
* @return mixed
*/
protected function execute(string $sql)
{
while ($this->connID->more_results()) {
$this->connID->next_result();
if ($res = $this->connID->store_result()) {
$res->free();
}
}
try {
return $this->connID->query($this->prepQuery($sql), $this->resultMode);
} catch (mysqli_sql_exception $e) {
log_message('error', $e->getMessage());
if ($this->DBDebug) {
throw $e;
}
}
return false;
}
/**
* Prep the query. If needed, each database adapter can prep the query string
*/
protected function prepQuery(string $sql): string
{
// mysqli_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack
// modifies the query so that it a proper number of affected rows is returned.
if ($this->deleteHack === true && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql)) {
return trim($sql) . ' WHERE 1=1';
}
return $sql;
}
/**
* Returns the total number of rows affected by this query.
*/
public function affectedRows(): int
{
return $this->connID->affected_rows ?? 0;
}
/**
* Platform-dependant string escape
*/
protected function _escapeString(string $str): string
{
if (! $this->connID) {
$this->initialize();
}
return $this->connID->real_escape_string($str);
}
/**
* Escape Like String Direct
* There are a few instances where MySQLi queries cannot take the
* additional "ESCAPE x" parameter for specifying the escape character
* in "LIKE" strings, and this handles those directly with a backslash.
*
* @param string|string[] $str Input string
*
* @return string|string[]
*/
public function escapeLikeStringDirect($str)
{
if (is_array($str)) {
foreach ($str as $key => $val) {
$str[$key] = $this->escapeLikeStringDirect($val);
}
return $str;
}
$str = $this->_escapeString($str);
// Escape LIKE condition wildcards
return str_replace(
[$this->likeEscapeChar, '%', '_'],
['\\' . $this->likeEscapeChar, '\\' . '%', '\\' . '_'],
$str
);
}
/**
* Generates the SQL for listing tables in a platform-dependent manner.
* Uses escapeLikeStringDirect().
*/
protected function _listTables(bool $prefixLimit = false): string
{
$sql = 'SHOW TABLES FROM ' . $this->escapeIdentifiers($this->database);
if ($prefixLimit !== false && $this->DBPrefix !== '') {
return $sql . " LIKE '" . $this->escapeLikeStringDirect($this->DBPrefix) . "%'";
}
return $sql;
}
/**
* Generates a platform-specific query string so that the column names can be fetched.
*/
protected function _listColumns(string $table = ''): string
{
return 'SHOW COLUMNS FROM ' . $this->protectIdentifiers($table, true, null, false);
}
/**
* Returns an array of objects with field data
*
* @throws DatabaseException
*
* @return stdClass[]
*/
protected function _fieldData(string $table): array
{
$table = $this->protectIdentifiers($table, true, null, false);
if (($query = $this->query('SHOW COLUMNS FROM ' . $table)) === false) {
throw new DatabaseException(lang('Database.failGetFieldData'));
}
$query = $query->getResultObject();
$retVal = [];
for ($i = 0, $c = count($query); $i < $c; $i++) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = $query[$i]->Field;
sscanf($query[$i]->Type, '%[a-z](%d)', $retVal[$i]->type, $retVal[$i]->max_length);
$retVal[$i]->nullable = $query[$i]->Null === 'YES';
$retVal[$i]->default = $query[$i]->Default;
$retVal[$i]->primary_key = (int) ($query[$i]->Key === 'PRI');
}
return $retVal;
}
/**
* Returns an array of objects with index data
*
* @throws DatabaseException
* @throws LogicException
*
* @return stdClass[]
*/
protected function _indexData(string $table): array
{
$table = $this->protectIdentifiers($table, true, null, false);
if (($query = $this->query('SHOW INDEX FROM ' . $table)) === false) {
throw new DatabaseException(lang('Database.failGetIndexData'));
}
if (! $indexes = $query->getResultArray()) {
return [];
}
$keys = [];
foreach ($indexes as $index) {
if (empty($keys[$index['Key_name']])) {
$keys[$index['Key_name']] = new stdClass();
$keys[$index['Key_name']]->name = $index['Key_name'];
if ($index['Key_name'] === 'PRIMARY') {
$type = 'PRIMARY';
} elseif ($index['Index_type'] === 'FULLTEXT') {
$type = 'FULLTEXT';
} elseif ($index['Non_unique']) {
$type = $index['Index_type'] === 'SPATIAL' ? 'SPATIAL' : 'INDEX';
} else {
$type = 'UNIQUE';
}
$keys[$index['Key_name']]->type = $type;
}
$keys[$index['Key_name']]->fields[] = $index['Column_name'];
}
return $keys;
}
/**
* Returns an array of objects with Foreign key data
*
* @throws DatabaseException
*
* @return stdClass[]
*/
protected function _foreignKeyData(string $table): array
{
$sql = '
SELECT
tc.CONSTRAINT_NAME,
tc.TABLE_NAME,
kcu.COLUMN_NAME,
rc.REFERENCED_TABLE_NAME,
kcu.REFERENCED_COLUMN_NAME
FROM information_schema.TABLE_CONSTRAINTS AS tc
INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS AS rc
ON tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
AND tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
INNER JOIN information_schema.KEY_COLUMN_USAGE AS kcu
ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
AND tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
WHERE
tc.CONSTRAINT_TYPE = ' . $this->escape('FOREIGN KEY') . ' AND
tc.TABLE_SCHEMA = ' . $this->escape($this->database) . ' AND
tc.TABLE_NAME = ' . $this->escape($table);
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetForeignKeyData'));
}
$query = $query->getResultObject();
$retVal = [];
foreach ($query as $row) {
$obj = new stdClass();
$obj->constraint_name = $row->CONSTRAINT_NAME;
$obj->table_name = $row->TABLE_NAME;
$obj->column_name = $row->COLUMN_NAME;
$obj->foreign_table_name = $row->REFERENCED_TABLE_NAME;
$obj->foreign_column_name = $row->REFERENCED_COLUMN_NAME;
$retVal[] = $obj;
}
return $retVal;
}
/**
* Returns platform-specific SQL to disable foreign key checks.
*
* @return string
*/
protected function _disableForeignKeyChecks()
{
return 'SET FOREIGN_KEY_CHECKS=0';
}
/**
* Returns platform-specific SQL to enable foreign key checks.
*
* @return string
*/
protected function _enableForeignKeyChecks()
{
return 'SET FOREIGN_KEY_CHECKS=1';
}
/**
* Returns the last error code and message.
* Must return this format: ['code' => string|int, 'message' => string]
* intval(code) === 0 means "no error".
*
* @return array<string, int|string>
*/
public function error(): array
{
if (! empty($this->mysqli->connect_errno)) {
return [
'code' => $this->mysqli->connect_errno,
'message' => $this->mysqli->connect_error,
];
}
return [
'code' => $this->connID->errno,
'message' => $this->connID->error,
];
}
/**
* Insert ID
*/
public function insertID(): int
{
return $this->connID->insert_id;
}
/**
* Begin Transaction
*/
protected function _transBegin(): bool
{
$this->connID->autocommit(false);
return $this->connID->begin_transaction();
}
/**
* Commit Transaction
*/
protected function _transCommit(): bool
{
if ($this->connID->commit()) {
$this->connID->autocommit(true);
return true;
}
return false;
}
/**
* Rollback Transaction
*/
protected function _transRollback(): bool
{
if ($this->connID->rollback()) {
$this->connID->autocommit(true);
return true;
}
return false;
}
}

View File

@@ -0,0 +1,239 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\Forge as BaseForge;
/**
* Forge for MySQLi
*/
class Forge extends BaseForge
{
/**
* CREATE DATABASE statement
*
* @var string
*/
protected $createDatabaseStr = 'CREATE DATABASE %s CHARACTER SET %s COLLATE %s';
/**
* CREATE DATABASE IF statement
*
* @var string
*/
protected $createDatabaseIfStr = 'CREATE DATABASE IF NOT EXISTS %s CHARACTER SET %s COLLATE %s';
/**
* DROP CONSTRAINT statement
*
* @var string
*/
protected $dropConstraintStr = 'ALTER TABLE %s DROP FOREIGN KEY %s';
/**
* CREATE TABLE keys flag
*
* Whether table keys are created from within the
* CREATE TABLE statement.
*
* @var bool
*/
protected $createTableKeys = true;
/**
* UNSIGNED support
*
* @var array
*/
protected $_unsigned = [
'TINYINT',
'SMALLINT',
'MEDIUMINT',
'INT',
'INTEGER',
'BIGINT',
'REAL',
'DOUBLE',
'DOUBLE PRECISION',
'FLOAT',
'DECIMAL',
'NUMERIC',
];
/**
* Table Options list which required to be quoted
*
* @var array
*/
protected $_quoted_table_options = [
'COMMENT',
'COMPRESSION',
'CONNECTION',
'DATA DIRECTORY',
'INDEX DIRECTORY',
'ENCRYPTION',
'PASSWORD',
];
/**
* NULL value representation in CREATE/ALTER TABLE statements
*
* @var string
*
* @internal
*/
protected $null = 'NULL';
/**
* CREATE TABLE attributes
*
* @param array $attributes Associative array of table attributes
*/
protected function _createTableAttributes(array $attributes): string
{
$sql = '';
foreach (array_keys($attributes) as $key) {
if (is_string($key)) {
$sql .= ' ' . strtoupper($key) . ' = ';
if (in_array(strtoupper($key), $this->_quoted_table_options, true)) {
$sql .= $this->db->escape($attributes[$key]);
} else {
$sql .= $this->db->escapeString($attributes[$key]);
}
}
}
if (! empty($this->db->charset) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) {
$sql .= ' DEFAULT CHARACTER SET = ' . $this->db->escapeString($this->db->charset);
}
if (! empty($this->db->DBCollat) && ! strpos($sql, 'COLLATE')) {
$sql .= ' COLLATE = ' . $this->db->escapeString($this->db->DBCollat);
}
return $sql;
}
/**
* ALTER TABLE
*
* @param string $alterType ALTER type
* @param string $table Table name
* @param mixed $field Column definition
*
* @return string|string[]
*/
protected function _alterTable(string $alterType, string $table, $field)
{
if ($alterType === 'DROP') {
return parent::_alterTable($alterType, $table, $field);
}
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
foreach ($field as $i => $data) {
if ($data['_literal'] !== false) {
$field[$i] = ($alterType === 'ADD') ? "\n\tADD " . $data['_literal'] : "\n\tMODIFY " . $data['_literal'];
} else {
if ($alterType === 'ADD') {
$field[$i]['_literal'] = "\n\tADD ";
} else {
$field[$i]['_literal'] = empty($data['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE ";
}
$field[$i] = $field[$i]['_literal'] . $this->_processColumn($field[$i]);
}
}
return [$sql . implode(',', $field)];
}
/**
* Process column
*/
protected function _processColumn(array $field): string
{
$extraClause = isset($field['after']) ? ' AFTER ' . $this->db->escapeIdentifiers($field['after']) : '';
if (empty($extraClause) && isset($field['first']) && $field['first'] === true) {
$extraClause = ' FIRST';
}
return $this->db->escapeIdentifiers($field['name'])
. (empty($field['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($field['new_name']))
. ' ' . $field['type'] . $field['length']
. $field['unsigned']
. $field['null']
. $field['default']
. $field['auto_increment']
. $field['unique']
. (empty($field['comment']) ? '' : ' COMMENT ' . $field['comment'])
. $extraClause;
}
/**
* Process indexes
*
* @param string $table (ignored)
*/
protected function _processIndexes(string $table): string
{
$sql = '';
for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
if (is_array($this->keys[$i])) {
for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) {
if (! isset($this->fields[$this->keys[$i][$i2]])) {
unset($this->keys[$i][$i2]);
continue;
}
}
} elseif (! isset($this->fields[$this->keys[$i]])) {
unset($this->keys[$i]);
continue;
}
if (! is_array($this->keys[$i])) {
$this->keys[$i] = [$this->keys[$i]];
}
$unique = in_array($i, $this->uniqueKeys, true) ? 'UNIQUE ' : '';
$sql .= ",\n\t{$unique}KEY " . $this->db->escapeIdentifiers(implode('_', $this->keys[$i]))
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ')';
}
$this->keys = [];
return $sql;
}
/**
* Drop Key
*
* @return bool
*/
public function dropKey(string $table, string $keyName)
{
$sql = sprintf(
$this->dropIndexStr,
$this->db->escapeIdentifiers($keyName),
$this->db->escapeIdentifiers($this->db->DBPrefix . $table),
);
return $this->db->query($sql);
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\MySQLi;
use BadMethodCallException;
use CodeIgniter\Database\BasePreparedQuery;
/**
* Prepared query for MySQLi
*/
class PreparedQuery extends BasePreparedQuery
{
/**
* Prepares the query against the database, and saves the connection
* info necessary to execute the query later.
*
* NOTE: This version is based on SQL code. Child classes should
* override this method.
*
* @param array $options Passed to the connection's prepare statement.
* Unused in the MySQLi driver.
*
* @return mixed
*/
public function _prepare(string $sql, array $options = [])
{
// Mysqli driver doesn't like statements
// with terminating semicolons.
$sql = rtrim($sql, ';');
if (! $this->statement = $this->db->mysqli->prepare($sql)) {
$this->errorCode = $this->db->mysqli->errno;
$this->errorString = $this->db->mysqli->error;
}
return $this;
}
/**
* Takes a new set of data and runs it against the currently
* prepared query. Upon success, will return a Results object.
*/
public function _execute(array $data): bool
{
if (! isset($this->statement)) {
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
}
// First off -bind the parameters
$bindTypes = '';
// Determine the type string
foreach ($data as $item) {
if (is_int($item)) {
$bindTypes .= 'i';
} elseif (is_numeric($item)) {
$bindTypes .= 'd';
} else {
$bindTypes .= 's';
}
}
// Bind it
$this->statement->bind_param($bindTypes, ...$data);
return $this->statement->execute();
}
/**
* Returns the result object for the prepared query.
*
* @return mixed
*/
public function _getResult()
{
return $this->statement->get_result();
}
}

View File

@@ -0,0 +1,162 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Entity\Entity;
use stdClass;
/**
* Result for MySQLi
*/
class Result extends BaseResult
{
/**
* Gets the number of fields in the result set.
*/
public function getFieldCount(): int
{
return $this->resultID->field_count;
}
/**
* Generates an array of column names in the result set.
*/
public function getFieldNames(): array
{
$fieldNames = [];
$this->resultID->field_seek(0);
while ($field = $this->resultID->fetch_field()) {
$fieldNames[] = $field->name;
}
return $fieldNames;
}
/**
* Generates an array of objects representing field meta-data.
*/
public function getFieldData(): array
{
static $dataTypes = [
MYSQLI_TYPE_DECIMAL => 'decimal',
MYSQLI_TYPE_NEWDECIMAL => 'newdecimal',
MYSQLI_TYPE_FLOAT => 'float',
MYSQLI_TYPE_DOUBLE => 'double',
MYSQLI_TYPE_BIT => 'bit',
MYSQLI_TYPE_SHORT => 'short',
MYSQLI_TYPE_LONG => 'long',
MYSQLI_TYPE_LONGLONG => 'longlong',
MYSQLI_TYPE_INT24 => 'int24',
MYSQLI_TYPE_YEAR => 'year',
MYSQLI_TYPE_TIMESTAMP => 'timestamp',
MYSQLI_TYPE_DATE => 'date',
MYSQLI_TYPE_TIME => 'time',
MYSQLI_TYPE_DATETIME => 'datetime',
MYSQLI_TYPE_NEWDATE => 'newdate',
MYSQLI_TYPE_SET => 'set',
MYSQLI_TYPE_VAR_STRING => 'var_string',
MYSQLI_TYPE_STRING => 'string',
MYSQLI_TYPE_GEOMETRY => 'geometry',
MYSQLI_TYPE_TINY_BLOB => 'tiny_blob',
MYSQLI_TYPE_MEDIUM_BLOB => 'medium_blob',
MYSQLI_TYPE_LONG_BLOB => 'long_blob',
MYSQLI_TYPE_BLOB => 'blob',
];
$retVal = [];
$fieldData = $this->resultID->fetch_fields();
foreach ($fieldData as $i => $data) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = $data->name;
$retVal[$i]->type = $data->type;
$retVal[$i]->type_name = in_array($data->type, [1, 247], true) ? 'char' : ($dataTypes[$data->type] ?? null);
$retVal[$i]->max_length = $data->max_length;
$retVal[$i]->primary_key = $data->flags & 2;
$retVal[$i]->length = $data->length;
$retVal[$i]->default = $data->def;
}
return $retVal;
}
/**
* Frees the current result.
*/
public function freeResult()
{
if (is_object($this->resultID)) {
$this->resultID->free();
$this->resultID = false;
}
}
/**
* Moves the internal pointer to the desired offset. This is called
* internally before fetching results to make sure the result set
* starts at zero.
*
* @return mixed
*/
public function dataSeek(int $n = 0)
{
return $this->resultID->data_seek($n);
}
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return mixed
*/
protected function fetchAssoc()
{
return $this->resultID->fetch_assoc();
}
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @return bool|Entity|object
*/
protected function fetchObject(string $className = 'stdClass')
{
if (is_subclass_of($className, Entity::class)) {
return empty($data = $this->fetchAssoc()) ? false : (new $className())->setAttributes($data);
}
return $this->resultID->fetch_object($className);
}
/**
* Returns the number of rows in the resultID (i.e., mysqli_result object)
*/
public function getNumRows(): int
{
if (! is_int($this->numRows)) {
$this->numRows = $this->resultID->num_rows;
}
return $this->numRows;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\MySQLi;
use CodeIgniter\Database\BaseUtils;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Utils for MySQLi
*/
class Utils extends BaseUtils
{
/**
* List databases statement
*
* @var string
*/
protected $listDatabases = 'SHOW DATABASES';
/**
* OPTIMIZE TABLE statement
*
* @var string
*/
protected $optimizeTable = 'OPTIMIZE TABLE %s';
/**
* Platform dependent version of the backup function.
*
* @return mixed
*/
public function _backup(?array $prefs = null)
{
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
}

View File

@@ -0,0 +1,313 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Postgre;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Builder for Postgre
*/
class Builder extends BaseBuilder
{
/**
* ORDER BY random keyword
*
* @var array
*/
protected $randomKeyword = [
'RANDOM()',
];
/**
* Specifies which sql statements
* support the ignore option.
*
* @var array
*/
protected $supportedIgnoreStatements = [
'insert' => 'ON CONFLICT DO NOTHING',
];
/**
* Checks if the ignore option is supported by
* the Database Driver for the specific statement.
*
* @return string
*/
protected function compileIgnore(string $statement)
{
$sql = parent::compileIgnore($statement);
if (! empty($sql)) {
$sql = ' ' . trim($sql);
}
return $sql;
}
/**
* ORDER BY
*
* @param string $direction ASC, DESC or RANDOM
*
* @return BaseBuilder
*/
public function orderBy(string $orderBy, string $direction = '', ?bool $escape = null)
{
$direction = strtoupper(trim($direction));
if ($direction === 'RANDOM') {
if (ctype_digit($orderBy)) {
$orderBy = (float) ($orderBy > 1 ? "0.{$orderBy}" : $orderBy);
}
if (is_float($orderBy)) {
$this->db->simpleQuery("SET SEED {$orderBy}");
}
$orderBy = $this->randomKeyword[0];
$direction = '';
$escape = false;
}
return parent::orderBy($orderBy, $direction, $escape);
}
/**
* Increments a numeric column by the specified value.
*
* @throws DatabaseException
*
* @return mixed
*/
public function increment(string $column, int $value = 1)
{
$column = $this->db->protectIdentifiers($column);
$sql = $this->_update($this->QBFrom[0], [$column => "to_number({$column}, '9999999') + {$value}"]);
return $this->db->query($sql, $this->binds, false);
}
/**
* Decrements a numeric column by the specified value.
*
* @throws DatabaseException
*
* @return mixed
*/
public function decrement(string $column, int $value = 1)
{
$column = $this->db->protectIdentifiers($column);
$sql = $this->_update($this->QBFrom[0], [$column => "to_number({$column}, '9999999') - {$value}"]);
return $this->db->query($sql, $this->binds, false);
}
/**
* Compiles an replace into string and runs the query.
* Because PostgreSQL doesn't support the replace into command,
* we simply do a DELETE and an INSERT on the first key/value
* combo, assuming that it's either the primary key or a unique key.
*
* @param array|null $set An associative array of insert values
*
* @throws DatabaseException
*
* @return mixed
*/
public function replace(?array $set = null)
{
if ($set !== null) {
$this->set($set);
}
if (! $this->QBSet) {
if (CI_DEBUG) {
throw new DatabaseException('You must use the "set" method to update an entry.');
}
return false; // @codeCoverageIgnore
}
$table = $this->QBFrom[0];
$set = $this->binds;
array_walk($set, static function (array &$item) {
$item = $item[0];
});
$key = array_key_first($set);
$value = $set[$key];
$builder = $this->db->table($table);
$exists = $builder->where($key, $value, true)->get()->getFirstRow();
if (empty($exists) && $this->testMode) {
$result = $this->getCompiledInsert();
} elseif (empty($exists)) {
$result = $builder->insert($set);
} elseif ($this->testMode) {
$result = $this->where($key, $value, true)->getCompiledUpdate();
} else {
array_shift($set);
$result = $builder->where($key, $value, true)->update($set);
}
unset($builder);
$this->resetWrite();
return $result;
}
/**
* Generates a platform-specific insert string from the supplied data
*/
protected function _insert(string $table, array $keys, array $unescapedKeys): string
{
return trim(sprintf('INSERT INTO %s (%s) VALUES (%s) %s', $table, implode(', ', $keys), implode(', ', $unescapedKeys), $this->compileIgnore('insert')));
}
/**
* Generates a platform-specific insert string from the supplied data.
*/
protected function _insertBatch(string $table, array $keys, array $values): string
{
return trim(sprintf('INSERT INTO %s (%s) VALUES %s %s', $table, implode(', ', $keys), implode(', ', $values), $this->compileIgnore('insert')));
}
/**
* Compiles a delete string and runs the query
*
* @param mixed $where
*
* @throws DatabaseException
*
* @return mixed
*/
public function delete($where = '', ?int $limit = null, bool $resetData = true)
{
if (! empty($limit) || ! empty($this->QBLimit)) {
throw new DatabaseException('PostgreSQL does not allow LIMITs on DELETE queries.');
}
return parent::delete($where, $limit, $resetData);
}
/**
* Generates a platform-specific LIMIT clause.
*/
protected function _limit(string $sql, bool $offsetIgnore = false): string
{
return $sql . ' LIMIT ' . $this->QBLimit . ($this->QBOffset ? " OFFSET {$this->QBOffset}" : '');
}
/**
* Generates a platform-specific update string from the supplied data
*
* @throws DatabaseException
*/
protected function _update(string $table, array $values): string
{
if (! empty($this->QBLimit)) {
throw new DatabaseException('Postgres does not support LIMITs with UPDATE queries.');
}
$this->QBOrderBy = [];
return parent::_update($table, $values);
}
/**
* Generates a platform-specific batch update string from the supplied data
*/
protected function _updateBatch(string $table, array $values, string $index): string
{
$ids = [];
$final = [];
foreach ($values as $val) {
$ids[] = $val[$index];
foreach (array_keys($val) as $field) {
if ($field !== $index) {
$final[$field] = $final[$field] ?? [];
$final[$field][] = "WHEN {$val[$index]} THEN {$val[$field]}";
}
}
}
$cases = '';
foreach ($final as $k => $v) {
$cases .= "{$k} = (CASE {$index}\n"
. implode("\n", $v)
. "\nELSE {$k} END), ";
}
$this->where("{$index} IN(" . implode(',', $ids) . ')', null, false);
return "UPDATE {$table} SET " . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere');
}
/**
* Generates a platform-specific delete string from the supplied data
*/
protected function _delete(string $table): string
{
$this->QBLimit = false;
return parent::_delete($table);
}
/**
* 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'
*/
protected function _truncate(string $table): string
{
return 'TRUNCATE ' . $table . ' RESTART IDENTITY';
}
/**
* Platform independent LIKE statement builder.
*
* In PostgreSQL, the ILIKE operator will perform case insensitive
* searches according to the current locale.
*
* @see https://www.postgresql.org/docs/9.2/static/functions-matching.html
*/
protected function _like_statement(?string $prefix, string $column, ?string $not, string $bind, bool $insensitiveSearch = false): string
{
$op = $insensitiveSearch === true ? 'ILIKE' : 'LIKE';
return "{$prefix} {$column} {$not} {$op} :{$bind}:";
}
/**
* Generates the JOIN portion of the query
*
* @return BaseBuilder
*/
public function join(string $table, string $cond, string $type = '', ?bool $escape = null)
{
if (! in_array('FULL OUTER', $this->joinTypes, true)) {
$this->joinTypes = array_merge($this->joinTypes, ['FULL OUTER']);
}
return parent::join($table, $cond, $type, $escape);
}
}

View File

@@ -0,0 +1,517 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Postgre;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use ErrorException;
use stdClass;
/**
* Connection for Postgre
*/
class Connection extends BaseConnection
{
/**
* Database driver
*
* @var string
*/
public $DBDriver = 'Postgre';
/**
* Database schema
*
* @var string
*/
public $schema = 'public';
/**
* Identifier escape character
*
* @var string
*/
public $escapeChar = '"';
/**
* Connect to the database.
*
* @return mixed
*/
public function connect(bool $persistent = false)
{
if (empty($this->DSN)) {
$this->buildDSN();
}
// Strip pgsql if exists
if (mb_strpos($this->DSN, 'pgsql:') === 0) {
$this->DSN = mb_substr($this->DSN, 6);
}
// Convert semicolons to spaces.
$this->DSN = str_replace(';', ' ', $this->DSN);
$this->connID = $persistent === true ? pg_pconnect($this->DSN) : pg_connect($this->DSN);
if ($this->connID !== false) {
if ($persistent === true && pg_connection_status($this->connID) === PGSQL_CONNECTION_BAD && pg_ping($this->connID) === false
) {
return false;
}
if (! empty($this->schema)) {
$this->simpleQuery("SET search_path TO {$this->schema},public");
}
if ($this->setClientEncoding($this->charset) === false) {
return false;
}
}
return $this->connID;
}
/**
* Keep or establish the connection if no queries have been sent for
* a length of time exceeding the server's idle timeout.
*/
public function reconnect()
{
if (pg_ping($this->connID) === false) {
$this->connID = false;
}
}
/**
* Close the database connection.
*/
protected function _close()
{
pg_close($this->connID);
}
/**
* Select a specific database table to use.
*/
public function setDatabase(string $databaseName): bool
{
return false;
}
/**
* Returns a string containing the version of the database being used.
*/
public function getVersion(): string
{
if (isset($this->dataCache['version'])) {
return $this->dataCache['version'];
}
if (! $this->connID || ($pgVersion = pg_version($this->connID)) === false) {
$this->initialize();
}
return isset($pgVersion['server']) ? $this->dataCache['version'] = $pgVersion['server'] : false;
}
/**
* Executes the query against the database.
*
* @return mixed
*/
protected function execute(string $sql)
{
try {
return pg_query($this->connID, $sql);
} catch (ErrorException $e) {
log_message('error', $e);
if ($this->DBDebug) {
throw $e;
}
}
return false;
}
/**
* Get the prefix of the function to access the DB.
*/
protected function getDriverFunctionPrefix(): string
{
return 'pg_';
}
/**
* Returns the total number of rows affected by this query.
*/
public function affectedRows(): int
{
return pg_affected_rows($this->resultID);
}
/**
* "Smart" Escape String
*
* Escapes data based on type
*
* @param mixed $str
*
* @return mixed
*/
public function escape($str)
{
if (! $this->connID) {
$this->initialize();
}
if (is_string($str) || (is_object($str) && method_exists($str, '__toString'))) {
return pg_escape_literal($this->connID, $str);
}
if (is_bool($str)) {
return $str ? 'TRUE' : 'FALSE';
}
return parent::escape($str);
}
/**
* Platform-dependant string escape
*/
protected function _escapeString(string $str): string
{
if (! $this->connID) {
$this->initialize();
}
return pg_escape_string($this->connID, $str);
}
/**
* Generates the SQL for listing tables in a platform-dependent manner.
*/
protected function _listTables(bool $prefixLimit = false): string
{
$sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \'' . $this->schema . "'";
if ($prefixLimit !== false && $this->DBPrefix !== '') {
return $sql . ' AND "table_name" LIKE \''
. $this->escapeLikeString($this->DBPrefix) . "%' "
. sprintf($this->likeEscapeStr, $this->likeEscapeChar);
}
return $sql;
}
/**
* Generates a platform-specific query string so that the column names can be fetched.
*/
protected function _listColumns(string $table = ''): string
{
return 'SELECT "column_name"
FROM "information_schema"."columns"
WHERE LOWER("table_name") = '
. $this->escape($this->DBPrefix . strtolower($table))
. ' ORDER BY "ordinal_position"';
}
/**
* Returns an array of objects with field data
*
* @throws DatabaseException
*
* @return stdClass[]
*/
protected function _fieldData(string $table): array
{
$sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default"
FROM "information_schema"."columns"
WHERE LOWER("table_name") = '
. $this->escape(strtolower($table))
. ' ORDER BY "ordinal_position"';
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetFieldData'));
}
$query = $query->getResultObject();
$retVal = [];
for ($i = 0, $c = count($query); $i < $c; $i++) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = $query[$i]->column_name;
$retVal[$i]->type = $query[$i]->data_type;
$retVal[$i]->default = $query[$i]->column_default;
$retVal[$i]->max_length = $query[$i]->character_maximum_length > 0 ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision;
}
return $retVal;
}
/**
* Returns an array of objects with index data
*
* @throws DatabaseException
*
* @return stdClass[]
*/
protected function _indexData(string $table): array
{
$sql = 'SELECT "indexname", "indexdef"
FROM "pg_indexes"
WHERE LOWER("tablename") = ' . $this->escape(strtolower($table)) . '
AND "schemaname" = ' . $this->escape('public');
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetIndexData'));
}
$query = $query->getResultObject();
$retVal = [];
foreach ($query as $row) {
$obj = new stdClass();
$obj->name = $row->indexname;
$_fields = explode(',', preg_replace('/^.*\((.+?)\)$/', '$1', trim($row->indexdef)));
$obj->fields = array_map(static function ($v) {
return trim($v);
}, $_fields);
if (strpos($row->indexdef, 'CREATE UNIQUE INDEX pk') === 0) {
$obj->type = 'PRIMARY';
} else {
$obj->type = (strpos($row->indexdef, 'CREATE UNIQUE') === 0) ? 'UNIQUE' : 'INDEX';
}
$retVal[$obj->name] = $obj;
}
return $retVal;
}
/**
* Returns an array of objects with Foreign key data
*
* @throws DatabaseException
*
* @return stdClass[]
*/
protected function _foreignKeyData(string $table): array
{
$sql = 'SELECT
tc.constraint_name, tc.table_name, kcu.column_name,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
WHERE constraint_type = ' . $this->escape('FOREIGN KEY') . ' AND
tc.table_name = ' . $this->escape($table);
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetForeignKeyData'));
}
$query = $query->getResultObject();
$retVal = [];
foreach ($query as $row) {
$obj = new stdClass();
$obj->constraint_name = $row->constraint_name;
$obj->table_name = $row->table_name;
$obj->column_name = $row->column_name;
$obj->foreign_table_name = $row->foreign_table_name;
$obj->foreign_column_name = $row->foreign_column_name;
$retVal[] = $obj;
}
return $retVal;
}
/**
* Returns platform-specific SQL to disable foreign key checks.
*
* @return string
*/
protected function _disableForeignKeyChecks()
{
return 'SET CONSTRAINTS ALL DEFERRED';
}
/**
* Returns platform-specific SQL to enable foreign key checks.
*
* @return string
*/
protected function _enableForeignKeyChecks()
{
return 'SET CONSTRAINTS ALL IMMEDIATE;';
}
/**
* Returns the last error code and message.
* Must return this format: ['code' => string|int, 'message' => string]
* intval(code) === 0 means "no error".
*
* @return array<string, int|string>
*/
public function error(): array
{
return [
'code' => '',
'message' => pg_last_error($this->connID) ?: '',
];
}
/**
* @return int|string
*/
public function insertID()
{
$v = pg_version($this->connID);
// 'server' key is only available since PostgreSQL 7.4
$v = explode(' ', $v['server'])[0] ?? 0;
$table = func_num_args() > 0 ? func_get_arg(0) : null;
$column = func_num_args() > 1 ? func_get_arg(1) : null;
if ($table === null && $v >= '8.1') {
$sql = 'SELECT LASTVAL() AS ins_id';
} elseif ($table !== null) {
if ($column !== null && $v >= '8.0') {
$sql = "SELECT pg_get_serial_sequence('{$table}', '{$column}') AS seq";
$query = $this->query($sql);
$query = $query->getRow();
$seq = $query->seq;
} else {
// seq_name passed in table parameter
$seq = $table;
}
$sql = "SELECT CURRVAL('{$seq}') AS ins_id";
} else {
return pg_last_oid($this->resultID);
}
$query = $this->query($sql);
$query = $query->getRow();
return (int) $query->ins_id;
}
/**
* Build a DSN from the provided parameters
*/
protected function buildDSN()
{
if ($this->DSN !== '') {
$this->DSN = '';
}
// If UNIX sockets are used, we shouldn't set a port
if (strpos($this->hostname, '/') !== false) {
$this->port = '';
}
if ($this->hostname !== '') {
$this->DSN = "host={$this->hostname} ";
}
// ctype_digit only accepts strings
$port = (string) $this->port;
if ($port !== '' && ctype_digit($port)) {
$this->DSN .= "port={$port} ";
}
if ($this->username !== '') {
$this->DSN .= "user={$this->username} ";
// An empty password is valid!
// password must be set to null to ignore it.
if ($this->password !== null) {
$this->DSN .= "password='{$this->password}' ";
}
}
if ($this->database !== '') {
$this->DSN .= "dbname={$this->database} ";
}
// We don't have these options as elements in our standard configuration
// array, but they might be set by parse_url() if the configuration was
// provided via string> Example:
//
// Postgre://username:password@localhost:5432/database?connect_timeout=5&sslmode=1
foreach (['connect_timeout', 'options', 'sslmode', 'service'] as $key) {
if (isset($this->{$key}) && is_string($this->{$key}) && $this->{$key} !== '') {
$this->DSN .= "{$key}='{$this->{$key}}' ";
}
}
$this->DSN = rtrim($this->DSN);
}
/**
* Set client encoding
*/
protected function setClientEncoding(string $charset): bool
{
return pg_set_client_encoding($this->connID, $charset) === 0;
}
/**
* Begin Transaction
*/
protected function _transBegin(): bool
{
return (bool) pg_query($this->connID, 'BEGIN');
}
/**
* Commit Transaction
*/
protected function _transCommit(): bool
{
return (bool) pg_query($this->connID, 'COMMIT');
}
/**
* Rollback Transaction
*/
protected function _transRollback(): bool
{
return (bool) pg_query($this->connID, 'ROLLBACK');
}
/**
* Determines if a query is a "write" type.
*
* Overrides BaseConnection::isWriteType, adding additional read query types.
*
* @param mixed $sql
*/
public function isWriteType($sql): bool
{
if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) {
return false;
}
return parent::isWriteType($sql);
}
}

View File

@@ -0,0 +1,193 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Postgre;
use CodeIgniter\Database\Forge as BaseForge;
/**
* Forge for Postgre
*/
class Forge extends BaseForge
{
/**
* CHECK DATABASE EXIST statement
*
* @var string
*/
protected $checkDatabaseExistStr = 'SELECT 1 FROM pg_database WHERE datname = ?';
/**
* DROP CONSTRAINT statement
*
* @var string
*/
protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s';
/**
* DROP INDEX statement
*
* @var string
*/
protected $dropIndexStr = 'DROP INDEX %s';
/**
* UNSIGNED support
*
* @var array
*/
protected $_unsigned = [
'INT2' => 'INTEGER',
'SMALLINT' => 'INTEGER',
'INT' => 'BIGINT',
'INT4' => 'BIGINT',
'INTEGER' => 'BIGINT',
'INT8' => 'NUMERIC',
'BIGINT' => 'NUMERIC',
'REAL' => 'DOUBLE PRECISION',
'FLOAT' => 'DOUBLE PRECISION',
];
/**
* NULL value representation in CREATE/ALTER TABLE statements
*
* @var string
*
* @internal
*/
protected $null = 'NULL';
/**
* CREATE TABLE attributes
*
* @param array $attributes Associative array of table attributes
*/
protected function _createTableAttributes(array $attributes): string
{
return '';
}
/**
* @param mixed $field
*
* @return array|bool|string
*/
protected function _alterTable(string $alterType, string $table, $field)
{
if (in_array($alterType, ['DROP', 'ADD'], true)) {
return parent::_alterTable($alterType, $table, $field);
}
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
$sqls = [];
foreach ($field as $data) {
if ($data['_literal'] !== false) {
return false;
}
if (version_compare($this->db->getVersion(), '8', '>=') && isset($data['type'])) {
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
. " TYPE {$data['type']}{$data['length']}";
}
if (! empty($data['default'])) {
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
. " SET DEFAULT {$data['default']}";
}
if (isset($data['null'])) {
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
. ($data['null'] === true ? ' DROP' : ' SET') . ' NOT NULL';
}
if (! empty($data['new_name'])) {
$sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($data['name'])
. ' TO ' . $this->db->escapeIdentifiers($data['new_name']);
}
if (! empty($data['comment'])) {
$sqls[] = 'COMMENT ON COLUMN' . $this->db->escapeIdentifiers($table)
. '.' . $this->db->escapeIdentifiers($data['name'])
. " IS {$data['comment']}";
}
}
return $sqls;
}
/**
* Process column
*/
protected function _processColumn(array $field): string
{
return $this->db->escapeIdentifiers($field['name'])
. ' ' . $field['type'] . $field['length']
. $field['default']
. $field['null']
. $field['auto_increment']
. $field['unique'];
}
/**
* Performs a data type mapping between different databases.
*/
protected function _attributeType(array &$attributes)
{
// Reset field lengths for data types that don't support it
if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== false) {
$attributes['CONSTRAINT'] = null;
}
switch (strtoupper($attributes['TYPE'])) {
case 'TINYINT':
$attributes['TYPE'] = 'SMALLINT';
$attributes['UNSIGNED'] = false;
break;
case 'MEDIUMINT':
$attributes['TYPE'] = 'INTEGER';
$attributes['UNSIGNED'] = false;
break;
case 'DATETIME':
$attributes['TYPE'] = 'TIMESTAMP';
break;
default:
break;
}
}
/**
* Field attribute AUTO_INCREMENT
*/
protected function _attributeAutoIncrement(array &$attributes, array &$field)
{
if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true) {
$field['type'] = $field['type'] === 'NUMERIC' || $field['type'] === 'BIGINT' ? 'BIGSERIAL' : 'SERIAL';
}
}
/**
* Generates a platform-specific DROP TABLE string
*/
protected function _dropTable(string $table, bool $ifExists, bool $cascade): string
{
$sql = parent::_dropTable($table, $ifExists, $cascade);
if ($cascade === true) {
$sql .= ' CASCADE';
}
return $sql;
}
}

View File

@@ -0,0 +1,111 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Postgre;
use BadMethodCallException;
use CodeIgniter\Database\BasePreparedQuery;
use Exception;
/**
* Prepared query for Postgre
*/
class PreparedQuery extends BasePreparedQuery
{
/**
* Stores the name this query can be
* used under by postgres. Only used internally.
*
* @var string
*/
protected $name;
/**
* The result resource from a successful
* pg_exec. Or false.
*
* @var bool|Result
*/
protected $result;
/**
* Prepares the query against the database, and saves the connection
* info necessary to execute the query later.
*
* NOTE: This version is based on SQL code. Child classes should
* override this method.
*
* @param array $options Passed to the connection's prepare statement.
* Unused in the MySQLi driver.
*
* @throws Exception
*
* @return mixed
*/
public function _prepare(string $sql, array $options = [])
{
$this->name = (string) random_int(1, 10000000000000000);
$sql = $this->parameterize($sql);
// Update the query object since the parameters are slightly different
// than what was put in.
$this->query->setQuery($sql);
if (! $this->statement = pg_prepare($this->db->connID, $this->name, $sql)) {
$this->errorCode = 0;
$this->errorString = pg_last_error($this->db->connID);
}
return $this;
}
/**
* Takes a new set of data and runs it against the currently
* prepared query. Upon success, will return a Results object.
*/
public function _execute(array $data): bool
{
if (! isset($this->statement)) {
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
}
$this->result = pg_execute($this->db->connID, $this->name, $data);
return (bool) $this->result;
}
/**
* Returns the result object for the prepared query.
*
* @return mixed
*/
public function _getResult()
{
return $this->result;
}
/**
* Replaces the ? placeholders with $1, $2, etc parameters for use
* within the prepared query.
*/
public function parameterize(string $sql): string
{
// Track our current value
$count = 0;
return preg_replace_callback('/\?/', static function () use (&$count) {
$count++;
return "\${$count}";
}, $sql);
}
}

View File

@@ -0,0 +1,128 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Postgre;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Entity\Entity;
use stdClass;
/**
* Result for Postgre
*/
class Result extends BaseResult
{
/**
* Gets the number of fields in the result set.
*/
public function getFieldCount(): int
{
return pg_num_fields($this->resultID);
}
/**
* Generates an array of column names in the result set.
*/
public function getFieldNames(): array
{
$fieldNames = [];
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
$fieldNames[] = pg_field_name($this->resultID, $i);
}
return $fieldNames;
}
/**
* Generates an array of objects representing field meta-data.
*/
public function getFieldData(): array
{
$retVal = [];
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = pg_field_name($this->resultID, $i);
$retVal[$i]->type = pg_field_type_oid($this->resultID, $i);
$retVal[$i]->type_name = pg_field_type($this->resultID, $i);
$retVal[$i]->max_length = pg_field_size($this->resultID, $i);
$retVal[$i]->length = $retVal[$i]->max_length;
// $retVal[$i]->primary_key = (int)($fieldData[$i]->flags & 2);
// $retVal[$i]->default = $fieldData[$i]->def;
}
return $retVal;
}
/**
* Frees the current result.
*/
public function freeResult()
{
if ($this->resultID !== false) {
pg_free_result($this->resultID);
$this->resultID = false;
}
}
/**
* Moves the internal pointer to the desired offset. This is called
* internally before fetching results to make sure the result set
* starts at zero.
*
* @return mixed
*/
public function dataSeek(int $n = 0)
{
return pg_result_seek($this->resultID, $n);
}
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return mixed
*/
protected function fetchAssoc()
{
return pg_fetch_assoc($this->resultID);
}
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @return bool|Entity|object
*/
protected function fetchObject(string $className = 'stdClass')
{
if (is_subclass_of($className, Entity::class)) {
return empty($data = $this->fetchAssoc()) ? false : (new $className())->setAttributes($data);
}
return pg_fetch_object($this->resultID, null, $className);
}
/**
* Returns the number of rows in the resultID (i.e., PostgreSQL query result resource)
*/
public function getNumRows(): int
{
if (! is_int($this->numRows)) {
$this->numRows = pg_num_rows($this->resultID);
}
return $this->numRows;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\Postgre;
use CodeIgniter\Database\BaseUtils;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Utils for Postgre
*/
class Utils extends BaseUtils
{
/**
* List databases statement
*
* @var string
*/
protected $listDatabases = 'SELECT datname FROM pg_database';
/**
* OPTIMIZE TABLE statement
*
* @var string
*/
protected $optimizeTable = 'REINDEX TABLE %s';
/**
* Platform dependent version of the backup function.
*
* @return mixed
*/
public function _backup(?array $prefs = null)
{
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
}

View File

@@ -0,0 +1,54 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
/**
* Prepared query interface
*/
interface PreparedQueryInterface
{
/**
* Takes a new set of data and runs it against the currently
* prepared query. Upon success, will return a Results object.
*
* @return ResultInterface
*/
public function execute(...$data);
/**
* Prepares the query against the database, and saves the connection
* info necessary to execute the query later.
*
* @return mixed
*/
public function prepare(string $sql, array $options = []);
/**
* Explicity closes the statement.
*/
public function close();
/**
* Returns the SQL that has been prepared.
*/
public function getQueryString(): string;
/**
* Returns the error code created while executing this statement.
*/
public function getErrorCode(): int;
/**
* Returns the error message created while executing this statement.
*/
public function getErrorMessage(): string;
}

418
system/Database/Query.php Normal file
View File

@@ -0,0 +1,418 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
/**
* Query builder
*/
class Query implements QueryInterface
{
/**
* The query string, as provided by the user.
*
* @var string
*/
protected $originalQueryString;
/**
* The final query string after binding, etc.
*
* @var string
*/
protected $finalQueryString;
/**
* The binds and their values used for binding.
*
* @var array
*/
protected $binds = [];
/**
* Bind marker
*
* Character used to identify values in a prepared statement.
*
* @var string
*/
protected $bindMarker = '?';
/**
* The start time in seconds with microseconds
* for when this query was executed.
*
* @var float|string
*/
protected $startTime;
/**
* The end time in seconds with microseconds
* for when this query was executed.
*
* @var float
*/
protected $endTime;
/**
* The error code, if any.
*
* @var int
*/
protected $errorCode;
/**
* The error message, if any.
*
* @var string
*/
protected $errorString;
/**
* Pointer to database connection.
* Mainly for escaping features.
*
* @var ConnectionInterface
*/
public $db;
public function __construct(ConnectionInterface &$db)
{
$this->db = $db;
}
/**
* Sets the raw query string to use for this statement.
*
* @param mixed $binds
*
* @return $this
*/
public function setQuery(string $sql, $binds = null, bool $setEscape = true)
{
$this->originalQueryString = $sql;
if ($binds !== null) {
if (! is_array($binds)) {
$binds = [$binds];
}
if ($setEscape) {
array_walk($binds, static function (&$item) {
$item = [
$item,
true,
];
});
}
$this->binds = $binds;
}
return $this;
}
/**
* Will store the variables to bind into the query later.
*
* @return $this
*/
public function setBinds(array $binds, bool $setEscape = true)
{
if ($setEscape) {
array_walk($binds, static function (&$item) {
$item = [$item, true];
});
}
$this->binds = $binds;
return $this;
}
/**
* Returns the final, processed query string after binding, etal
* has been performed.
*/
public function getQuery(): string
{
if (empty($this->finalQueryString)) {
$this->finalQueryString = $this->originalQueryString;
}
$this->compileBinds();
return $this->finalQueryString;
}
/**
* Records the execution time of the statement using microtime(true)
* for it's start and end values. If no end value is present, will
* use the current time to determine total duration.
*
* @param float $end
*
* @return $this
*/
public function setDuration(float $start, ?float $end = null)
{
$this->startTime = $start;
if ($end === null) {
$end = microtime(true);
}
$this->endTime = $end;
return $this;
}
/**
* Returns the start time in seconds with microseconds.
*
* @return float|string
*/
public function getStartTime(bool $returnRaw = false, int $decimals = 6)
{
if ($returnRaw) {
return $this->startTime;
}
return number_format($this->startTime, $decimals);
}
/**
* Returns the duration of this query during execution, or null if
* the query has not been executed yet.
*
* @param int $decimals The accuracy of the returned time.
*/
public function getDuration(int $decimals = 6): string
{
return number_format(($this->endTime - $this->startTime), $decimals);
}
/**
* Stores the error description that happened for this query.
*
* @return $this
*/
public function setError(int $code, string $error)
{
$this->errorCode = $code;
$this->errorString = $error;
return $this;
}
/**
* Reports whether this statement created an error not.
*/
public function hasError(): bool
{
return ! empty($this->errorString);
}
/**
* Returns the error code created while executing this statement.
*/
public function getErrorCode(): int
{
return $this->errorCode;
}
/**
* Returns the error message created while executing this statement.
*/
public function getErrorMessage(): string
{
return $this->errorString;
}
/**
* Determines if the statement is a write-type query or not.
*/
public function isWriteType(): bool
{
return $this->db->isWriteType($this->originalQueryString);
}
/**
* Swaps out one table prefix for a new one.
*
* @return $this
*/
public function swapPrefix(string $orig, string $swap)
{
$sql = empty($this->finalQueryString) ? $this->originalQueryString : $this->finalQueryString;
$this->finalQueryString = preg_replace('/(\W)' . $orig . '(\S+?)/', '\\1' . $swap . '\\2', $sql);
return $this;
}
/**
* Returns the original SQL that was passed into the system.
*/
public function getOriginalQuery(): string
{
return $this->originalQueryString;
}
/**
* Escapes and inserts any binds into the finalQueryString object.
*
* @see https://regex101.com/r/EUEhay/5
*/
protected function compileBinds()
{
$sql = $this->finalQueryString;
$binds = $this->binds;
if (empty($binds)) {
return;
}
if (is_int(array_key_first($binds))) {
$bindCount = count($binds);
$ml = strlen($this->bindMarker);
$this->finalQueryString = $this->matchSimpleBinds($sql, $binds, $bindCount, $ml);
} else {
// Reverse the binds so that duplicate named binds
// will be processed prior to the original binds.
$binds = array_reverse($binds);
$this->finalQueryString = $this->matchNamedBinds($sql, $binds);
}
}
/**
* Match bindings
*/
protected function matchNamedBinds(string $sql, array $binds): string
{
$replacers = [];
foreach ($binds as $placeholder => $value) {
// $value[1] contains the boolean whether should be escaped or not
$escapedValue = $value[1] ? $this->db->escape($value[0]) : $value[0];
// In order to correctly handle backlashes in saved strings
// we will need to preg_quote, so remove the wrapping escape characters
// otherwise it will get escaped.
if (is_array($value[0])) {
$escapedValue = '(' . implode(',', $escapedValue) . ')';
}
$replacers[":{$placeholder}:"] = $escapedValue;
}
return strtr($sql, $replacers);
}
/**
* Match bindings
*/
protected function matchSimpleBinds(string $sql, array $binds, int $bindCount, int $ml): string
{
if ($c = preg_match_all("/'[^']*'/", $sql, $matches)) {
$c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', str_replace($matches[0], str_replace($this->bindMarker, str_repeat(' ', $ml), $matches[0]), $sql, $c), $matches, PREG_OFFSET_CAPTURE);
// Bind values' count must match the count of markers in the query
if ($bindCount !== $c) {
return $sql;
}
} elseif (($c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bindCount) {
return $sql;
}
do {
$c--;
$escapedValue = $binds[$c][1] ? $this->db->escape($binds[$c][0]) : $binds[$c][0];
if (is_array($escapedValue)) {
$escapedValue = '(' . implode(',', $escapedValue) . ')';
}
$sql = substr_replace($sql, $escapedValue, $matches[0][$c][1], $ml);
} while ($c !== 0);
return $sql;
}
/**
* Returns string to display in debug toolbar
*/
public function debugToolbarDisplay(): string
{
// Key words we want bolded
static $highlight = [
'AND',
'AS',
'ASC',
'AVG',
'BY',
'COUNT',
'DESC',
'DISTINCT',
'FROM',
'GROUP',
'HAVING',
'IN',
'INNER',
'INSERT',
'INTO',
'IS',
'JOIN',
'LEFT',
'LIKE',
'LIMIT',
'MAX',
'MIN',
'NOT',
'NULL',
'OFFSET',
'ON',
'OR',
'ORDER',
'RIGHT',
'SELECT',
'SUM',
'UPDATE',
'VALUES',
'WHERE',
];
if (empty($this->finalQueryString)) {
$this->compileBinds(); // @codeCoverageIgnore
}
$sql = esc($this->finalQueryString);
/**
* @see https://stackoverflow.com/a/20767160
* @see https://regex101.com/r/hUlrGN/4
*/
$search = '/\b(?:' . implode('|', $highlight) . ')\b(?![^(&#039;)]*&#039;(?:(?:[^(&#039;)]*&#039;){2})*[^(&#039;)]*$)/';
return preg_replace_callback($search, static function ($matches) {
return '<strong>' . str_replace(' ', '&nbsp;', $matches[0]) . '</strong>';
}, $sql);
}
/**
* Return text representation of the query
*/
public function __toString(): string
{
return $this->getQuery();
}
}

View File

@@ -0,0 +1,87 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
/**
* Interface QueryInterface
*
* Represents a single statement that can be executed against the database.
* Statements are platform-specific and can handle binding of binds.
*/
interface QueryInterface
{
/**
* Sets the raw query string to use for this statement.
*
* @param mixed $binds
*
* @return mixed
*/
public function setQuery(string $sql, $binds = null, bool $setEscape = true);
/**
* Returns the final, processed query string after binding, etal
* has been performed.
*
* @return mixed
*/
public function getQuery();
/**
* Records the execution time of the statement using microtime(true)
* for it's start and end values. If no end value is present, will
* use the current time to determine total duration.
*
* @return mixed
*/
public function setDuration(float $start, ?float $end = null);
/**
* Returns the duration of this query during execution, or null if
* the query has not been executed yet.
*
* @param int $decimals The accuracy of the returned time.
*/
public function getDuration(int $decimals = 6): string;
/**
* Stores the error description that happened for this query.
*/
public function setError(int $code, string $error);
/**
* Reports whether this statement created an error not.
*/
public function hasError(): bool;
/**
* Returns the error code created while executing this statement.
*/
public function getErrorCode(): int;
/**
* Returns the error message created while executing this statement.
*/
public function getErrorMessage(): string;
/**
* Determines if the statement is a write-type query or not.
*/
public function isWriteType(): bool;
/**
* Swaps out one table prefix for a new one.
*
* @return mixed
*/
public function swapPrefix(string $orig, string $swap);
}

View File

@@ -0,0 +1,164 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
/**
* Interface ResultInterface
*/
interface ResultInterface
{
/**
* Retrieve the results of the query. Typically an array of
* individual data rows, which can be either an 'array', an
* 'object', or a custom class name.
*
* @param string $type The row type. Either 'array', 'object', or a class name to use
*/
public function getResult(string $type = 'object'): array;
/**
* Returns the results as an array of custom objects.
*
* @param string $className The name of the class to use.
*
* @return mixed
*/
public function getCustomResultObject(string $className);
/**
* Returns the results as an array of arrays.
*
* If no results, an empty array is returned.
*/
public function getResultArray(): array;
/**
* Returns the results as an array of objects.
*
* If no results, an empty array is returned.
*/
public function getResultObject(): array;
/**
* Wrapper object to return a row as either an array, an object, or
* a custom class.
*
* If row doesn't exist, returns null.
*
* @param mixed $n The index of the results to return
* @param string $type The type of result object. 'array', 'object' or class name.
*
* @return mixed
*/
public function getRow($n = 0, string $type = 'object');
/**
* Returns a row as a custom class instance.
*
* If row doesn't exists, returns null.
*
* @return mixed
*/
public function getCustomRowObject(int $n, string $className);
/**
* Returns a single row from the results as an array.
*
* If row doesn't exist, returns null.
*
* @return mixed
*/
public function getRowArray(int $n = 0);
/**
* Returns a single row from the results as an object.
*
* If row doesn't exist, returns null.
*
* @return mixed
*/
public function getRowObject(int $n = 0);
/**
* Assigns an item into a particular column slot.
*
* @param string $key
* @param mixed $value
*
* @return mixed
*/
public function setRow($key, $value = null);
/**
* Returns the "first" row of the current results.
*
* @return mixed
*/
public function getFirstRow(string $type = 'object');
/**
* Returns the "last" row of the current results.
*
* @return mixed
*/
public function getLastRow(string $type = 'object');
/**
* Returns the "next" row of the current results.
*
* @return mixed
*/
public function getNextRow(string $type = 'object');
/**
* Returns the "previous" row of the current results.
*
* @return mixed
*/
public function getPreviousRow(string $type = 'object');
/**
* Returns an unbuffered row and move the pointer to the next row.
*
* @return mixed
*/
public function getUnbufferedRow(string $type = 'object');
/**
* Gets the number of fields in the result set.
*/
public function getFieldCount(): int;
/**
* Generates an array of column names in the result set.
*/
public function getFieldNames(): array;
/**
* Generates an array of objects representing field meta-data.
*/
public function getFieldData(): array;
/**
* Frees the current result.
*/
public function freeResult();
/**
* Moves the internal pointer to the desired offset. This is called
* internally before fetching results to make sure the result set
* starts at zero.
*
* @return mixed
*/
public function dataSeek(int $n = 0);
}

View File

@@ -0,0 +1,625 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLSRV;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Exceptions\DataException;
use CodeIgniter\Database\ResultInterface;
/**
* Builder for SQLSRV
*
* @todo auto check for TextCastToInt
* @todo auto check for InsertIndexValue
* @todo replace: delete index entries before insert
*/
class Builder extends BaseBuilder
{
/**
* ORDER BY random keyword
*
* @var array
*/
protected $randomKeyword = [
'NEWID()',
'RAND(%d)',
];
/**
* Quoted identifier flag
*
* Whether to use SQL-92 standard quoted identifier
* (double quotes) or brackets for identifier escaping.
*
* @var bool
*/
protected $_quoted_identifier = true;
/**
* Handle increment/decrement on text
*
* @var bool
*/
public $castTextToInt = true;
/**
* Handle IDENTITY_INSERT property/
*
* @var bool
*/
public $keyPermission = false;
/**
* Groups tables in FROM clauses if needed, so there is no confusion
* about operator precedence.
*/
protected function _fromTables(): string
{
$from = [];
foreach ($this->QBFrom as $value) {
$from[] = $this->getFullName($value);
}
return implode(', ', $from);
}
/**
* 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'
*/
protected function _truncate(string $table): string
{
return 'TRUNCATE TABLE ' . $this->getFullName($table);
}
/**
* Generates the JOIN portion of the query
*
* @return $this
*/
public function join(string $table, string $cond, string $type = '', ?bool $escape = null)
{
if ($type !== '') {
$type = strtoupper(trim($type));
if (! in_array($type, $this->joinTypes, 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);
if (! 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];
}
ksort($conditions);
} else {
$conditions = [$cond];
$joints = [''];
}
$cond = ' ON ';
foreach ($conditions as $i => $condition) {
$operator = $this->getOperator($condition);
$cond .= $joints[$i];
$cond .= preg_match('/(\(*)?([\[\]\w\.\'-]+)' . preg_quote($operator, '/') . '(.*)/i', $condition, $match) ? $match[1] . $this->db->protectIdentifiers($match[2]) . $operator . $this->db->protectIdentifiers($match[3]) : $condition;
}
}
// 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[] = $type . 'JOIN ' . $this->getFullName($table) . $cond;
return $this;
}
/**
* Generates a platform-specific insert string from the supplied data
*
* @todo implement check for this instead static $insertKeyPermission
*/
protected function _insert(string $table, array $keys, array $unescapedKeys): string
{
$fullTableName = $this->getFullName($table);
// insert statement
$statement = 'INSERT INTO ' . $fullTableName . ' (' . implode(',', $keys) . ') VALUES (' . implode(', ', $unescapedKeys) . ')';
return $this->keyPermission ? $this->addIdentity($fullTableName, $statement) : $statement;
}
/**
* Insert batch statement
*
* Generates a platform-specific insert string from the supplied data.
*/
protected function _insertBatch(string $table, array $keys, array $values): string
{
return 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $this->getFullName($table) . ' (' . implode(', ', $keys) . ') VALUES ' . implode(', ', $values);
}
/**
* Generates a platform-specific update string from the supplied data
*/
protected function _update(string $table, array $values): string
{
$valstr = [];
foreach ($values as $key => $val) {
$valstr[] = $key . ' = ' . $val;
}
$fullTableName = $this->getFullName($table);
$statement = sprintf('UPDATE %s%s SET ', empty($this->QBLimit) ? '' : 'TOP(' . $this->QBLimit . ') ', $fullTableName);
$statement .= implode(', ', $valstr)
. $this->compileWhereHaving('QBWhere')
. $this->compileOrderBy();
return $this->keyPermission ? $this->addIdentity($fullTableName, $statement) : $statement;
}
/**
* Update_Batch statement
*
* Generates a platform-specific batch update string from the supplied data
*/
protected function _updateBatch(string $table, array $values, string $index): string
{
$ids = [];
$final = [];
foreach ($values as $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 ' . $this->compileIgnore('update') . ' ' . $this->getFullName($table) . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere');
}
/**
* Increments a numeric column by the specified value.
*
* @return bool
*/
public function increment(string $column, int $value = 1)
{
$column = $this->db->protectIdentifiers($column);
if ($this->castTextToInt) {
$values = [$column => "CONVERT(VARCHAR(MAX),CONVERT(INT,CONVERT(VARCHAR(MAX), {$column})) + {$value})"];
} else {
$values = [$column => "{$column} + {$value}"];
}
$sql = $this->_update($this->QBFrom[0], $values);
return $this->db->query($sql, $this->binds, false);
}
/**
* Decrements a numeric column by the specified value.
*
* @return bool
*/
public function decrement(string $column, int $value = 1)
{
$column = $this->db->protectIdentifiers($column);
if ($this->castTextToInt) {
$values = [$column => "CONVERT(VARCHAR(MAX),CONVERT(INT,CONVERT(VARCHAR(MAX), {$column})) - {$value})"];
} else {
$values = [$column => "{$column} + {$value}"];
}
$sql = $this->_update($this->QBFrom[0], $values);
return $this->db->query($sql, $this->binds, false);
}
/**
* Get full name of the table
*/
private function getFullName(string $table): string
{
$alias = '';
if (strpos($table, ' ') !== false) {
$alias = explode(' ', $table);
$table = array_shift($alias);
$alias = ' ' . implode(' ', $alias);
}
if ($this->db->escapeChar === '"') {
return '"' . $this->db->getDatabase() . '"."' . $this->db->schema . '"."' . str_replace('"', '', $table) . '"' . $alias;
}
return '[' . $this->db->getDatabase() . '].[' . $this->db->schema . '].[' . str_replace('"', '', $table) . ']' . str_replace('"', '', $alias);
}
/**
* Add permision statements for index value inserts
*/
private function addIdentity(string $fullTable, string $insert): string
{
return 'SET IDENTITY_INSERT ' . $fullTable . " ON\n" . $insert . "\nSET IDENTITY_INSERT " . $fullTable . ' OFF';
}
/**
* Local implementation of limit
*/
protected function _limit(string $sql, bool $offsetIgnore = false): string
{
if (empty($this->QBOrderBy)) {
$sql .= ' ORDER BY (SELECT NULL) ';
}
if ($offsetIgnore) {
$sql .= ' OFFSET 0 ';
} else {
$sql .= is_int($this->QBOffset) ? ' OFFSET ' . $this->QBOffset : ' OFFSET 0 ';
}
return $sql . ' ROWS FETCH NEXT ' . $this->QBLimit . ' ROWS ONLY ';
}
/**
* Compiles a replace into string and runs the query
*
* @throws DatabaseException
*
* @return mixed
*/
public function replace(?array $set = null)
{
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; // @codeCoverageIgnore
}
$table = $this->QBFrom[0];
$sql = $this->_replace($table, array_keys($this->QBSet), array_values($this->QBSet));
$this->resetWrite();
if ($this->testMode) {
return $sql;
}
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->getFullName($table) . ' ON');
$result = $this->db->query($sql, $this->binds, false);
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->getFullName($table) . ' OFF');
return $result;
}
/**
* Generates a platform-specific replace string from the supplied data
* on match delete and insert
*/
protected function _replace(string $table, array $keys, array $values): string
{
// check whether the existing keys are part of the primary key.
// if so then use them for the "ON" part and exclude them from the $values and $keys
$pKeys = $this->db->getIndexData($table);
$keyFields = [];
foreach ($pKeys as $key) {
if ($key->type === 'PRIMARY') {
$keyFields = array_merge($keyFields, $key->fields);
}
if ($key->type === 'UNIQUE') {
$keyFields = array_merge($keyFields, $key->fields);
}
}
// Get the unique field names
$escKeyFields = array_map(function (string $field): string {
return $this->db->protectIdentifiers($field);
}, array_values(array_unique($keyFields)));
// Get the binds
$binds = $this->binds;
array_walk($binds, static function (&$item) {
$item = $item[0];
});
// Get the common field and values from the keys data and index fields
$common = array_intersect($keys, $escKeyFields);
$bingo = [];
foreach ($common as $v) {
$k = array_search($v, $keys, true);
$bingo[$keys[$k]] = $binds[trim($values[$k], ':')];
}
// Querying existing data
$builder = $this->db->table($table);
foreach ($bingo as $k => $v) {
$builder->where($k, $v);
}
$q = $builder->get()->getResult();
// Delete entries if we find them
if ($q !== []) {
$delete = $this->db->table($table);
foreach ($bingo as $k => $v) {
$delete->where($k, $v);
}
$delete->delete();
}
return sprintf('INSERT INTO %s (%s) VALUES (%s);', $this->getFullName($table), implode(',', $keys), implode(',', $values));
}
/**
* SELECT [MAX|MIN|AVG|SUM|COUNT]()
*
* Handle float return value
*
* @return BaseBuilder
*/
protected function maxMinAvgSum(string $select = '', string $alias = '', string $type = 'MAX')
{
// int functions can be handled by parent
if ($type !== 'AVG') {
return parent::maxMinAvgSum($select, $alias, $type);
}
if ($select === '') {
throw DataException::forEmptyInputGiven('Select');
}
if (strpos($select, ',') !== false) {
throw DataException::forInvalidArgument('Column name not separated by comma');
}
if ($alias === '') {
$alias = $this->createAliasFromTable(trim($select));
}
$sql = $type . '( CAST( ' . $this->db->protectIdentifiers(trim($select)) . ' AS FLOAT ) ) AS ' . $this->db->escapeIdentifiers(trim($alias));
$this->QBSelect[] = $sql;
$this->QBNoEscape[] = null;
return $this;
}
/**
* "Count All" query
*
* Generates a platform-specific query string that counts all records in
* the particular table
*
* @param bool $reset Are we want to clear query builder values?
*
* @return int|string when $test = true
*/
public function countAll(bool $reset = true)
{
$table = $this->QBFrom[0];
$sql = $this->countString . $this->db->escapeIdentifiers('numrows') . ' FROM ' . $this->getFullName($table);
if ($this->testMode) {
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;
}
/**
* Delete statement
*/
protected function _delete(string $table): string
{
return 'DELETE' . (empty($this->QBLimit) ? '' : ' TOP (' . $this->QBLimit . ') ') . ' FROM ' . $this->getFullName($table) . $this->compileWhereHaving('QBWhere');
}
/**
* Compiles a delete string and runs the query
*
* @param mixed $where
*
* @throws DatabaseException
*
* @return mixed
*/
public function delete($where = '', ?int $limit = null, bool $resetData = true)
{
$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; // @codeCoverageIgnore
}
if (! empty($limit)) {
$this->QBLimit = $limit;
}
$sql = $this->_delete($table);
if ($resetData) {
$this->resetWrite();
}
return $this->testMode ? $sql : $this->db->query($sql, $this->binds, false);
}
/**
* Compile the SELECT statement
*
* Generates a query string based on which functions were used.
*
* @param bool $selectOverride
*/
protected function compileSelect($selectOverride = false): string
{
// Write the "select" portion of the query
if ($selectOverride !== false) {
$sql = $selectOverride;
} else {
$sql = (! $this->QBDistinct) ? 'SELECT ' : 'SELECT DISTINCT ';
// SQL Server can't work with select * if group by is specified
if (empty($this->QBSelect) && ! empty($this->QBGroupBy) && is_array($this->QBGroupBy)) {
foreach ($this->QBGroupBy as $field) {
$this->QBSelect[] = is_array($field) ? $field['field'] : $field;
}
}
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) {
$noEscape = $this->QBNoEscape[$key] ?? null;
$this->QBSelect[$key] = $this->db->protectIdentifiers($val, false, $noEscape);
}
$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) {
$sql = $this->_limit($sql . "\n");
}
return $sql;
}
/**
* Compiles the select statement based on the other functions called
* and runs the query
*
* @return ResultInterface
*/
public function get(?int $limit = null, int $offset = 0, bool $reset = true)
{
if ($limit !== null) {
$this->limit($limit, $offset);
}
$result = $this->testMode ? $this->getCompiledSelect($reset) : $this->db->query($this->compileSelect(), $this->binds, false);
if ($reset) {
$this->resetSelect();
// Clear our binds so we don't eat up memory
$this->binds = [];
}
return $result;
}
}

View File

@@ -0,0 +1,537 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLSRV;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use Exception;
use stdClass;
/**
* Connection for SQLSRV
*/
class Connection extends BaseConnection
{
/**
* Database driver
*
* @var string
*/
public $DBDriver = 'SQLSRV';
/**
* Database name
*
* @var string
*/
public $database;
/**
* Scrollable flag
*
* Determines what cursor type to use when executing queries.
*
* FALSE or SQLSRV_CURSOR_FORWARD would increase performance,
* but would disable num_rows() (and possibly insert_id())
*
* @var mixed
*/
public $scrollable;
/**
* Identifier escape character
*
* @var string
*/
public $escapeChar = '"';
/**
* Database schema
*
* @var string
*/
public $schema = 'dbo';
/**
* Quoted identifier flag
*
* Whether to use SQL-92 standard quoted identifier
* (double quotes) or brackets for identifier escaping.
*
* @var bool
*/
protected $_quoted_identifier = true;
/**
* List of reserved identifiers
*
* Identifiers that must NOT be escaped.
*
* @var string[]
*/
protected $_reserved_identifiers = ['*'];
/**
* Class constructor
*/
public function __construct(array $params)
{
parent::__construct($params);
// This is only supported as of SQLSRV 3.0
if ($this->scrollable === null) {
$this->scrollable = defined('SQLSRV_CURSOR_CLIENT_BUFFERED') ? SQLSRV_CURSOR_CLIENT_BUFFERED : false;
}
}
/**
* Connect to the database.
*
* @throws DatabaseException
*
* @return mixed
*/
public function connect(bool $persistent = false)
{
$charset = in_array(strtolower($this->charset), ['utf-8', 'utf8'], true) ? 'UTF-8' : SQLSRV_ENC_CHAR;
$connection = [
'UID' => empty($this->username) ? '' : $this->username,
'PWD' => empty($this->password) ? '' : $this->password,
'Database' => $this->database,
'ConnectionPooling' => $persistent ? 1 : 0,
'CharacterSet' => $charset,
'Encrypt' => $this->encrypt === true ? 1 : 0,
'ReturnDatesAsStrings' => 1,
];
// If the username and password are both empty, assume this is a
// 'Windows Authentication Mode' connection.
if (empty($connection['UID']) && empty($connection['PWD'])) {
unset($connection['UID'], $connection['PWD']);
}
sqlsrv_configure('WarningsReturnAsErrors', 0);
$this->connID = sqlsrv_connect($this->hostname, $connection);
if ($this->connID !== false) {
// Determine how identifiers are escaped
$query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi');
$query = $query->getResultObject();
$this->_quoted_identifier = empty($query) ? false : (bool) $query[0]->qi;
$this->escapeChar = ($this->_quoted_identifier) ? '"' : ['[', ']'];
return $this->connID;
}
$errors = [];
foreach (sqlsrv_errors(SQLSRV_ERR_ERRORS) as $error) {
$errors[] = preg_replace('/(\[.+\]\[.+\](?:\[.+\])?)(.+)/', '$2', $error['message']);
}
throw new DatabaseException(implode("\n", $errors));
}
/**
* Keep or establish the connection if no queries have been sent for
* a length of time exceeding the server's idle timeout.
*/
public function reconnect()
{
$this->close();
$this->initialize();
}
/**
* Close the database connection.
*/
protected function _close()
{
sqlsrv_close($this->connID);
}
/**
* Platform-dependant string escape
*/
protected function _escapeString(string $str): string
{
return str_replace("'", "''", remove_invisible_characters($str, false));
}
/**
* Insert ID
*/
public function insertID(): int
{
return $this->query('SELECT SCOPE_IDENTITY() AS insert_id')->getRow()->insert_id ?? 0;
}
/**
* Generates the SQL for listing tables in a platform-dependent manner.
*/
protected function _listTables(bool $prefixLimit = false): string
{
$sql = 'SELECT [TABLE_NAME] AS "name"'
. ' FROM [INFORMATION_SCHEMA].[TABLES] '
. ' WHERE '
. " [TABLE_SCHEMA] = '" . $this->schema . "' ";
if ($prefixLimit === true && $this->DBPrefix !== '') {
$sql .= " AND [TABLE_NAME] LIKE '" . $this->escapeLikeString($this->DBPrefix) . "%' "
. sprintf($this->likeEscapeStr, $this->likeEscapeChar);
}
return $sql;
}
/**
* Generates a platform-specific query string so that the column names can be fetched.
*/
protected function _listColumns(string $table = ''): string
{
return 'SELECT [COLUMN_NAME] '
. ' FROM [INFORMATION_SCHEMA].[COLUMNS]'
. ' WHERE [TABLE_NAME] = ' . $this->escape($this->DBPrefix . $table)
. ' AND [TABLE_SCHEMA] = ' . $this->escape($this->schema);
}
/**
* Returns an array of objects with index data
*
* @throws DatabaseException
*
* @return stdClass[]
*/
protected function _indexData(string $table): array
{
$sql = 'EXEC sp_helpindex ' . $this->escape($this->schema . '.' . $table);
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetIndexData'));
}
$query = $query->getResultObject();
$retVal = [];
foreach ($query as $row) {
$obj = new stdClass();
$obj->name = $row->index_name;
$_fields = explode(',', trim($row->index_keys));
$obj->fields = array_map(static function ($v) {
return trim($v);
}, $_fields);
if (strpos($row->index_description, 'primary key located on') !== false) {
$obj->type = 'PRIMARY';
} else {
$obj->type = (strpos($row->index_description, 'nonclustered, unique') !== false) ? 'UNIQUE' : 'INDEX';
}
$retVal[$obj->name] = $obj;
}
return $retVal;
}
/**
* Returns an array of objects with Foreign key data
* referenced_object_id parent_object_id
*
* @throws DatabaseException
*
* @return stdClass[]
*/
protected function _foreignKeyData(string $table): array
{
$sql = 'SELECT '
. 'f.name as constraint_name, '
. 'OBJECT_NAME (f.parent_object_id) as table_name, '
. 'COL_NAME(fc.parent_object_id,fc.parent_column_id) column_name, '
. 'OBJECT_NAME(f.referenced_object_id) foreign_table_name, '
. 'COL_NAME(fc.referenced_object_id,fc.referenced_column_id) foreign_column_name '
. 'FROM '
. 'sys.foreign_keys AS f '
. 'INNER JOIN '
. 'sys.foreign_key_columns AS fc '
. 'ON f.OBJECT_ID = fc.constraint_object_id '
. 'INNER JOIN '
. 'sys.tables t '
. 'ON t.OBJECT_ID = fc.referenced_object_id '
. 'WHERE '
. 'OBJECT_NAME (f.parent_object_id) = ' . $this->escape($table);
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetForeignKeyData'));
}
$query = $query->getResultObject();
$retVal = [];
foreach ($query as $row) {
$obj = new stdClass();
$obj->constraint_name = $row->constraint_name;
$obj->table_name = $row->table_name;
$obj->column_name = $row->column_name;
$obj->foreign_table_name = $row->foreign_table_name;
$obj->foreign_column_name = $row->foreign_column_name;
$retVal[] = $obj;
}
return $retVal;
}
/**
* Disables foreign key checks temporarily.
*
* @return string
*/
protected function _disableForeignKeyChecks()
{
return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT ALL"';
}
/**
* Enables foreign key checks temporarily.
*
* @return string
*/
protected function _enableForeignKeyChecks()
{
return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL"';
}
/**
* Returns an array of objects with field data
*
* @throws DatabaseException
*
* @return stdClass[]
*/
protected function _fieldData(string $table): array
{
$sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME= ' . $this->escape(($table));
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetFieldData'));
}
$query = $query->getResultObject();
$retVal = [];
for ($i = 0, $c = count($query); $i < $c; $i++) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = $query[$i]->COLUMN_NAME;
$retVal[$i]->type = $query[$i]->DATA_TYPE;
$retVal[$i]->default = $query[$i]->COLUMN_DEFAULT;
$retVal[$i]->max_length = $query[$i]->CHARACTER_MAXIMUM_LENGTH > 0
? $query[$i]->CHARACTER_MAXIMUM_LENGTH
: $query[$i]->NUMERIC_PRECISION;
}
return $retVal;
}
/**
* Begin Transaction
*/
protected function _transBegin(): bool
{
return sqlsrv_begin_transaction($this->connID);
}
/**
* Commit Transaction
*/
protected function _transCommit(): bool
{
return sqlsrv_commit($this->connID);
}
/**
* Rollback Transaction
*/
protected function _transRollback(): bool
{
return sqlsrv_rollback($this->connID);
}
/**
* Returns the last error code and message.
* Must return this format: ['code' => string|int, 'message' => string]
* intval(code) === 0 means "no error".
*
* @return array<string, int|string>
*/
public function error(): array
{
$error = [
'code' => '00000',
'message' => '',
];
$sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
if (! is_array($sqlsrvErrors)) {
return $error;
}
$sqlsrvError = array_shift($sqlsrvErrors);
if (isset($sqlsrvError['SQLSTATE'])) {
$error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
} elseif (isset($sqlsrvError['code'])) {
$error['code'] = $sqlsrvError['code'];
}
if (isset($sqlsrvError['message'])) {
$error['message'] = $sqlsrvError['message'];
}
return $error;
}
/**
* Returns the total number of rows affected by this query.
*/
public function affectedRows(): int
{
return sqlsrv_rows_affected($this->resultID);
}
/**
* Select a specific database table to use.
*
* @return mixed
*/
public function setDatabase(?string $databaseName = null)
{
if (empty($databaseName)) {
$databaseName = $this->database;
}
if (empty($this->connID)) {
$this->initialize();
}
if ($this->execute('USE ' . $this->_escapeString($databaseName))) {
$this->database = $databaseName;
$this->dataCache = [];
return true;
}
return false;
}
/**
* Executes the query against the database.
*
* @return mixed
*/
protected function execute(string $sql)
{
$stmt = ($this->scrollable === false || $this->isWriteType($sql)) ?
sqlsrv_query($this->connID, $sql) :
sqlsrv_query($this->connID, $sql, [], ['Scrollable' => $this->scrollable]);
if ($stmt === false) {
$error = $this->error();
log_message('error', $error['message']);
if ($this->DBDebug) {
throw new Exception($error['message']);
}
}
return $stmt;
}
/**
* Returns the last error encountered by this connection.
*
* @return mixed
*/
public function getError()
{
$error = [
'code' => '00000',
'message' => '',
];
$sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
if (! is_array($sqlsrvErrors)) {
return $error;
}
$sqlsrvError = array_shift($sqlsrvErrors);
if (isset($sqlsrvError['SQLSTATE'])) {
$error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
} elseif (isset($sqlsrvError['code'])) {
$error['code'] = $sqlsrvError['code'];
}
if (isset($sqlsrvError['message'])) {
$error['message'] = $sqlsrvError['message'];
}
return $error;
}
/**
* The name of the platform in use (MySQLi, mssql, etc)
*/
public function getPlatform(): string
{
return $this->DBDriver;
}
/**
* Returns a string containing the version of the database being used.
*/
public function getVersion(): string
{
if (isset($this->dataCache['version'])) {
return $this->dataCache['version'];
}
if (! $this->connID || empty($info = sqlsrv_server_info($this->connID))) {
$this->initialize();
}
return isset($info['SQLServerVersion']) ? $this->dataCache['version'] = $info['SQLServerVersion'] : false;
}
/**
* Determines if a query is a "write" type.
*
* Overrides BaseConnection::isWriteType, adding additional read query types.
*
* @param mixed $sql
*/
public function isWriteType($sql): bool
{
if (preg_match('/^\s*"?(EXEC\s*sp_rename)\s/i', $sql)) {
return true;
}
return parent::isWriteType($sql);
}
}

View File

@@ -0,0 +1,405 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLSRV;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Forge as BaseForge;
/**
* Forge for SQLSRV
*/
class Forge extends BaseForge
{
/**
* DROP CONSTRAINT statement
*
* @var string
*/
protected $dropConstraintStr;
/**
* DROP INDEX statement
*
* @var string
*/
protected $dropIndexStr;
/**
* CREATE DATABASE IF statement
*
* @todo missing charset, collat & check for existent
*
* @var string
*/
protected $createDatabaseIfStr = "DECLARE @DBName VARCHAR(255) = '%s'\nDECLARE @SQL VARCHAR(max) = 'IF DB_ID( ''' + @DBName + ''' ) IS NULL CREATE DATABASE ' + @DBName\nEXEC( @SQL )";
/**
* CREATE DATABASE IF statement
*
* @todo missing charset & collat
*
* @var string
*/
protected $createDatabaseStr = 'CREATE DATABASE %s ';
/**
* CHECK DATABASE EXIST statement
*
* @var string
*/
protected $checkDatabaseExistStr = 'IF DB_ID( %s ) IS NOT NULL SELECT 1';
/**
* RENAME TABLE statement
*
* While the below statement would work, it returns an error.
* Also MS recommends dropping and dropping and re-creating the table.
*
* @see https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-rename-transact-sql?view=sql-server-2017
* 'EXEC sp_rename %s , %s ;'
*
* @var string
*/
protected $renameTableStr;
/**
* UNSIGNED support
*
* @var array
*/
protected $unsigned = [
'TINYINT' => 'SMALLINT',
'SMALLINT' => 'INT',
'INT' => 'BIGINT',
'REAL' => 'FLOAT',
];
/**
* CREATE TABLE IF statement
*
* @var string
*/
protected $createTableIfStr;
/**
* CREATE TABLE statement
*
* @var string
*/
protected $createTableStr;
public function __construct(BaseConnection $db)
{
parent::__construct($db);
$this->createTableIfStr = 'IF NOT EXISTS'
. '(SELECT t.name, s.name as schema_name, t.type_desc '
. 'FROM sys.tables t '
. 'INNER JOIN sys.schemas s on s.schema_id = t.schema_id '
. "WHERE s.name=N'" . $this->db->schema . "' "
. "AND t.name=REPLACE(N'%s', '\"', '') "
. "AND t.type_desc='USER_TABLE')\nCREATE TABLE ";
$this->createTableStr = '%s ' . $this->db->escapeIdentifiers($this->db->schema) . ".%s (%s\n) ";
$this->renameTableStr = 'EXEC sp_rename [' . $this->db->escapeIdentifiers($this->db->schema) . '.%s] , %s ;';
$this->dropConstraintStr = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.%s DROP CONSTRAINT %s';
$this->dropIndexStr = 'DROP INDEX %s ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.%s';
}
/**
* CREATE TABLE attributes
*/
protected function _createTableAttributes(array $attributes): string
{
return '';
}
/**
* @param mixed $field
*
* @return false|string|string[]
*/
protected function _alterTable(string $alterType, string $table, $field)
{
// Handle DROP here
if ($alterType === 'DROP') {
// check if fields are part of any indexes
$indexData = $this->db->getIndexData($table);
foreach ($indexData as $index) {
if (is_string($field)) {
$field = explode(',', $field);
}
$fld = array_intersect($field, $index->fields);
// Drop index if field is part of an index
if (! empty($fld)) {
$this->_dropIndex($table, $index);
}
}
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table) . ' DROP ';
$fields = array_map(static function ($item) {
return 'COLUMN [' . trim($item) . ']';
}, (array) $field);
return $sql . implode(',', $fields);
}
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table);
$sql .= ($alterType === 'ADD') ? 'ADD ' : ' ';
$sqls = [];
if ($alterType === 'ADD') {
foreach ($field as $data) {
$sqls[] = $sql . ($data['_literal'] !== false ? $data['_literal'] : $this->_processColumn($data));
}
return $sqls;
}
foreach ($field as $data) {
if ($data['_literal'] !== false) {
return false;
}
if (isset($data['type'])) {
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
. " {$data['type']}{$data['length']}";
}
if (! empty($data['default'])) {
$sqls[] = $sql . ' ALTER COLUMN ADD CONSTRAINT ' . $this->db->escapeIdentifiers($data['name']) . '_def'
. " DEFAULT {$data['default']} FOR " . $this->db->escapeIdentifiers($data['name']);
}
if (isset($data['null'])) {
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
. ($data['null'] === true ? ' DROP' : '') . " {$data['type']}{$data['length']} NOT NULL";
}
if (! empty($data['comment'])) {
$sqls[] = 'EXEC sys.sp_addextendedproperty '
. "@name=N'Caption', @value=N'" . $data['comment'] . "' , "
. "@level0type=N'SCHEMA',@level0name=N'" . $this->db->schema . "', "
. "@level1type=N'TABLE',@level1name=N'" . $this->db->escapeIdentifiers($table) . "', "
. "@level2type=N'COLUMN',@level2name=N'" . $this->db->escapeIdentifiers($data['name']) . "'";
}
if (! empty($data['new_name'])) {
$sqls[] = "EXEC sp_rename '[" . $this->db->schema . '].[' . $table . '].[' . $data['name'] . "]' , '" . $data['new_name'] . "', 'COLUMN';";
}
}
return $sqls;
}
/**
* Drop index for table
*
* @return mixed
*/
protected function _dropIndex(string $table, object $indexData)
{
if ($indexData->type === 'PRIMARY') {
$sql = 'ALTER TABLE [' . $this->db->schema . '].[' . $table . '] DROP [' . $indexData->name . ']';
} else {
$sql = 'DROP INDEX [' . $indexData->name . '] ON [' . $this->db->schema . '].[' . $table . ']';
}
return $this->db->simpleQuery($sql);
}
/**
* Process indexes
*
* @return array|string
*/
protected function _processIndexes(string $table)
{
$sqls = [];
for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
$this->keys[$i] = (array) $this->keys[$i];
for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) {
if (! isset($this->fields[$this->keys[$i][$i2]])) {
unset($this->keys[$i][$i2]);
}
}
if (count($this->keys[$i]) <= 0) {
continue;
}
if (in_array($i, $this->uniqueKeys, true)) {
$sqls[] = 'ALTER TABLE '
. $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table)
. ' ADD CONSTRAINT ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
. ' UNIQUE (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
continue;
}
$sqls[] = 'CREATE INDEX '
. $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
. ' ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table)
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
}
return $sqls;
}
/**
* Process column
*/
protected function _processColumn(array $field): string
{
return $this->db->escapeIdentifiers($field['name'])
. (empty($field['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($field['new_name']))
. ' ' . $field['type'] . $field['length']
. $field['default']
. $field['null']
. $field['auto_increment']
. ''
. $field['unique'];
}
/**
* Process foreign keys
*
* @param string $table Table name
*/
protected function _processForeignKeys(string $table): string
{
$sql = '';
$allowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT'];
foreach ($this->foreignKeys as $fkey) {
$nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign';
$nameIndexFilled = $this->db->escapeIdentifiers($nameIndex);
$foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field']));
$referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']);
$referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField']));
$formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)";
$sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled);
if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) {
$sql .= ' ON DELETE ' . $fkey['onDelete'];
}
if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) {
$sql .= ' ON UPDATE ' . $fkey['onUpdate'];
}
}
return $sql;
}
/**
* Process primary keys
*/
protected function _processPrimaryKeys(string $table): string
{
for ($i = 0, $c = count($this->primaryKeys); $i < $c; $i++) {
if (! isset($this->fields[$this->primaryKeys[$i]])) {
unset($this->primaryKeys[$i]);
}
}
if ($this->primaryKeys !== []) {
$sql = ",\n\tCONSTRAINT " . $this->db->escapeIdentifiers('pk_' . $table)
. ' PRIMARY KEY(' . implode(', ', $this->db->escapeIdentifiers($this->primaryKeys)) . ')';
}
return $sql ?? '';
}
/**
* Performs a data type mapping between different databases.
*/
protected function _attributeType(array &$attributes)
{
// Reset field lengths for data types that don't support it
if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== false) {
$attributes['CONSTRAINT'] = null;
}
switch (strtoupper($attributes['TYPE'])) {
case 'MEDIUMINT':
$attributes['TYPE'] = 'INTEGER';
$attributes['UNSIGNED'] = false;
break;
case 'INTEGER':
$attributes['TYPE'] = 'INT';
break;
case 'ENUM':
$attributes['TYPE'] = 'TEXT';
$attributes['CONSTRAINT'] = null;
break;
case 'TIMESTAMP':
$attributes['TYPE'] = 'DATETIME';
break;
case 'BOOLEAN':
$attributes['TYPE'] = 'BIT';
break;
default:
break;
}
}
/**
* Field attribute AUTO_INCREMENT
*/
protected function _attributeAutoIncrement(array &$attributes, array &$field)
{
if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true && stripos($field['type'], 'INT') !== false) {
$field['auto_increment'] = ' IDENTITY(1,1)';
}
}
/**
* Generates a platform-specific DROP TABLE string
*
* @todo Support for cascade
*/
protected function _dropTable(string $table, bool $ifExists, bool $cascade): string
{
$sql = 'DROP TABLE';
if ($ifExists) {
$sql .= ' IF EXISTS ';
}
$table = ' [' . $this->db->database . '].[' . $this->db->schema . '].[' . $table . '] ';
$sql .= $table;
if ($cascade) {
$sql .= '';
}
return $sql;
}
}

View File

@@ -0,0 +1,114 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLSRV;
use BadMethodCallException;
use CodeIgniter\Database\BasePreparedQuery;
use Exception;
/**
* Prepared query for Postgre
*/
class PreparedQuery extends BasePreparedQuery
{
/**
* Parameters array used to store the dynamic variables.
*
* @var array
*/
protected $parameters = [];
/**
* The result boolean from a sqlsrv_execute.
*
* @var bool
*/
protected $result;
/**
* Prepares the query against the database, and saves the connection
* info necessary to execute the query later.
*
* NOTE: This version is based on SQL code. Child classes should
* override this method.
*
* @param array $options Options takes an associative array;
*
* @throws Exception
*
* @return mixed
*/
public function _prepare(string $sql, array $options = [])
{
// Prepare parameters for the query
$queryString = $this->getQueryString();
$parameters = $this->parameterize($queryString);
// Prepare the query
$this->statement = sqlsrv_prepare($this->db->connID, $sql, $parameters);
if (! $this->statement) {
$info = $this->db->error();
$this->errorCode = $info['code'];
$this->errorString = $info['message'];
}
return $this;
}
/**
* Takes a new set of data and runs it against the currently
* prepared query. Upon success, will return a Results object.
*/
public function _execute(array $data): bool
{
if (! isset($this->statement)) {
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
}
foreach ($data as $key => $value) {
$this->parameters[$key] = $value;
}
$this->result = sqlsrv_execute($this->statement);
return (bool) $this->result;
}
/**
* Returns the result object for the prepared query.
*
* @return mixed
*/
public function _getResult()
{
return $this->result;
}
/**
* Handle parameters
*/
protected function parameterize(string $queryString): array
{
$numberOfVariables = substr_count($queryString, '?');
$params = [];
for ($c = 0; $c < $numberOfVariables; $c++) {
$this->parameters[$c] = null;
$params[] = &$this->parameters[$c];
}
return $params;
}
}

View File

@@ -0,0 +1,170 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLSRV;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Entity\Entity;
use stdClass;
/**
* Result for SQLSRV
*/
class Result extends BaseResult
{
/**
* Gets the number of fields in the result set.
*/
public function getFieldCount(): int
{
return @sqlsrv_num_fields($this->resultID);
}
/**
* Generates an array of column names in the result set.
*/
public function getFieldNames(): array
{
$fieldNames = [];
foreach (sqlsrv_field_metadata($this->resultID) as $field) {
$fieldNames[] = $field['Name'];
}
return $fieldNames;
}
/**
* Generates an array of objects representing field meta-data.
*/
public function getFieldData(): array
{
static $dataTypes = [
SQLSRV_SQLTYPE_BIGINT => 'bigint',
SQLSRV_SQLTYPE_BIT => 'bit',
SQLSRV_SQLTYPE_CHAR => 'char',
SQLSRV_SQLTYPE_DATE => 'date',
SQLSRV_SQLTYPE_DATETIME => 'datetime',
SQLSRV_SQLTYPE_DATETIME2 => 'datetime2',
SQLSRV_SQLTYPE_DATETIMEOFFSET => 'datetimeoffset',
SQLSRV_SQLTYPE_DECIMAL => 'decimal',
SQLSRV_SQLTYPE_FLOAT => 'float',
SQLSRV_SQLTYPE_IMAGE => 'image',
SQLSRV_SQLTYPE_INT => 'int',
SQLSRV_SQLTYPE_MONEY => 'money',
SQLSRV_SQLTYPE_NCHAR => 'nchar',
SQLSRV_SQLTYPE_NUMERIC => 'numeric',
SQLSRV_SQLTYPE_NVARCHAR => 'nvarchar',
SQLSRV_SQLTYPE_NTEXT => 'ntext',
SQLSRV_SQLTYPE_REAL => 'real',
SQLSRV_SQLTYPE_SMALLDATETIME => 'smalldatetime',
SQLSRV_SQLTYPE_SMALLINT => 'smallint',
SQLSRV_SQLTYPE_SMALLMONEY => 'smallmoney',
SQLSRV_SQLTYPE_TEXT => 'text',
SQLSRV_SQLTYPE_TIME => 'time',
SQLSRV_SQLTYPE_TIMESTAMP => 'timestamp',
SQLSRV_SQLTYPE_TINYINT => 'tinyint',
SQLSRV_SQLTYPE_UNIQUEIDENTIFIER => 'uniqueidentifier',
SQLSRV_SQLTYPE_UDT => 'udt',
SQLSRV_SQLTYPE_VARBINARY => 'varbinary',
SQLSRV_SQLTYPE_VARCHAR => 'varchar',
SQLSRV_SQLTYPE_XML => 'xml',
];
$retVal = [];
foreach (sqlsrv_field_metadata($this->resultID) as $i => $field) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = $field['Name'];
$retVal[$i]->type = $field['Type'];
$retVal[$i]->type_name = $dataTypes[$field['Type']] ?? null;
$retVal[$i]->max_length = $field['Size'];
}
return $retVal;
}
/**
* Frees the current result.
*/
public function freeResult()
{
if (is_resource($this->resultID)) {
sqlsrv_free_stmt($this->resultID);
$this->resultID = false;
}
}
/**
* Moves the internal pointer to the desired offset. This is called
* internally before fetching results to make sure the result set
* starts at zero.
*
* @return mixed
*/
public function dataSeek(int $n = 0)
{
if ($n > 0) {
for ($i = 0; $i < $n; $i++) {
if (sqlsrv_fetch($this->resultID) === false) {
return false;
}
}
}
return true;
}
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return mixed
*/
protected function fetchAssoc()
{
return sqlsrv_fetch_array($this->resultID, SQLSRV_FETCH_ASSOC);
}
/**
* Returns the result set as an object.
*
* @return bool|Entity|object
*/
protected function fetchObject(string $className = 'stdClass')
{
if (is_subclass_of($className, Entity::class)) {
return empty($data = $this->fetchAssoc()) ? false : (new $className())->setAttributes($data);
}
return sqlsrv_fetch_object($this->resultID, $className);
}
/**
* Returns the number of rows in the resultID (i.e., SQLSRV query result resource)
*/
public function getNumRows(): int
{
if (! is_int($this->numRows)) {
$this->numRows = sqlsrv_num_rows($this->resultID);
}
return $this->numRows;
}
}

View File

@@ -0,0 +1,53 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLSRV;
use CodeIgniter\Database\BaseUtils;
use CodeIgniter\Database\ConnectionInterface;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Utils for SQLSRV
*/
class Utils extends BaseUtils
{
/**
* List databases statement
*
* @var string
*/
protected $listDatabases = 'EXEC sp_helpdb'; // Can also be: EXEC sp_databases
/**
* OPTIMIZE TABLE statement
*
* @var string
*/
protected $optimizeTable = 'ALTER INDEX all ON %s REORGANIZE';
public function __construct(ConnectionInterface &$db)
{
parent::__construct($db);
$this->optimizeTable = 'ALTER INDEX all ON ' . $this->db->schema . '.%s REORGANIZE';
}
/**
* Platform dependent version of the backup function.
*
* @return mixed
*/
public function _backup(?array $prefs = null)
{
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
}

View File

@@ -0,0 +1,73 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use CodeIgniter\Database\BaseBuilder;
/**
* Builder for SQLite3
*/
class Builder extends BaseBuilder
{
/**
* Default installs of SQLite typically do not
* support limiting delete clauses.
*
* @var bool
*/
protected $canLimitDeletes = false;
/**
* Default installs of SQLite do no support
* limiting update queries in combo with WHERE.
*
* @var bool
*/
protected $canLimitWhereUpdates = false;
/**
* ORDER BY random keyword
*
* @var array
*/
protected $randomKeyword = [
'RANDOM()',
];
/**
* @var array
*/
protected $supportedIgnoreStatements = [
'insert' => 'OR IGNORE',
];
/**
* Replace statement
*
* Generates a platform-specific replace string from the supplied data
*/
protected function _replace(string $table, array $keys, array $values): string
{
return 'INSERT OR ' . parent::_replace($table, $keys, $values);
}
/**
* Generates a platform-specific truncate string from the supplied data
*
* If the database does not support the TRUNCATE statement,
* then this method maps to 'DELETE FROM table'
*/
protected function _truncate(string $table): string
{
return 'DELETE FROM ' . $table;
}
}

View File

@@ -0,0 +1,396 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use ErrorException;
use Exception;
use SQLite3;
use stdClass;
/**
* Connection for SQLite3
*/
class Connection extends BaseConnection
{
/**
* Database driver
*
* @var string
*/
public $DBDriver = 'SQLite3';
/**
* Identifier escape character
*
* @var string
*/
public $escapeChar = '`';
/**
* Connect to the database.
*
* @throws DatabaseException
*
* @return mixed
*/
public function connect(bool $persistent = false)
{
if ($persistent && $this->DBDebug) {
throw new DatabaseException('SQLite3 doesn\'t support persistent connections.');
}
try {
if ($this->database !== ':memory:' && strpos($this->database, DIRECTORY_SEPARATOR) === false) {
$this->database = WRITEPATH . $this->database;
}
return (! $this->password)
? new SQLite3($this->database)
: new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
} catch (Exception $e) {
throw new DatabaseException('SQLite3 error: ' . $e->getMessage());
}
}
/**
* Keep or establish the connection if no queries have been sent for
* a length of time exceeding the server's idle timeout.
*/
public function reconnect()
{
$this->close();
$this->initialize();
}
/**
* Close the database connection.
*/
protected function _close()
{
$this->connID->close();
}
/**
* Select a specific database table to use.
*/
public function setDatabase(string $databaseName): bool
{
return false;
}
/**
* Returns a string containing the version of the database being used.
*/
public function getVersion(): string
{
if (isset($this->dataCache['version'])) {
return $this->dataCache['version'];
}
$version = SQLite3::version();
return $this->dataCache['version'] = $version['versionString'];
}
/**
* Execute the query
*
* @return mixed \SQLite3Result object or bool
*/
protected function execute(string $sql)
{
try {
return $this->isWriteType($sql)
? $this->connID->exec($sql)
: $this->connID->query($sql);
} catch (ErrorException $e) {
log_message('error', $e);
if ($this->DBDebug) {
throw $e;
}
}
return false;
}
/**
* Returns the total number of rows affected by this query.
*/
public function affectedRows(): int
{
return $this->connID->changes();
}
/**
* Platform-dependant string escape
*/
protected function _escapeString(string $str): string
{
return $this->connID->escapeString($str);
}
/**
* Generates the SQL for listing tables in a platform-dependent manner.
*/
protected function _listTables(bool $prefixLimit = false): string
{
return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
. ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
. (($prefixLimit !== false && $this->DBPrefix !== '')
? ' AND "NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . '%\' ' . sprintf($this->likeEscapeStr, $this->likeEscapeChar)
: '');
}
/**
* Generates a platform-specific query string so that the column names can be fetched.
*/
protected function _listColumns(string $table = ''): string
{
return 'PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')';
}
/**
* @throws DatabaseException
*
* @return array|false
*/
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();
}
$sql = $this->_listColumns($table);
$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';
} elseif (isset($row['name'])) {
$key = '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];
}
/**
* Returns an array of objects with field data
*
* @throws DatabaseException
*
* @return stdClass[]
*/
protected function _fieldData(string $table): array
{
if (false === $query = $this->query('PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')')) {
throw new DatabaseException(lang('Database.failGetFieldData'));
}
$query = $query->getResultObject();
if (empty($query)) {
return [];
}
$retVal = [];
for ($i = 0, $c = count($query); $i < $c; $i++) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = $query[$i]->name;
$retVal[$i]->type = $query[$i]->type;
$retVal[$i]->max_length = null;
$retVal[$i]->default = $query[$i]->dflt_value;
$retVal[$i]->primary_key = isset($query[$i]->pk) && (bool) $query[$i]->pk;
$retVal[$i]->nullable = isset($query[$i]->notnull) && ! (bool) $query[$i]->notnull;
}
return $retVal;
}
/**
* Returns an array of objects with index data
*
* @throws DatabaseException
*
* @return stdClass[]
*/
protected function _indexData(string $table): array
{
// Get indexes
// Don't use PRAGMA index_list, so we can preserve index order
$sql = "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name=" . $this->escape(strtolower($table));
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetIndexData'));
}
$query = $query->getResultObject();
$retVal = [];
foreach ($query as $row) {
$obj = new stdClass();
$obj->name = $row->name;
// Get fields for index
$obj->fields = [];
if (false === $fields = $this->query('PRAGMA index_info(' . $this->escape(strtolower($row->name)) . ')')) {
throw new DatabaseException(lang('Database.failGetIndexData'));
}
$fields = $fields->getResultObject();
foreach ($fields as $field) {
$obj->fields[] = $field->name;
}
$retVal[$obj->name] = $obj;
}
return $retVal;
}
/**
* Returns an array of objects with Foreign key data
*
* @return stdClass[]
*/
protected function _foreignKeyData(string $table): array
{
if ($this->supportsForeignKeys() !== true) {
return [];
}
$tables = $this->listTables();
if (empty($tables)) {
return [];
}
$retVal = [];
foreach ($tables as $table) {
$query = $this->query("PRAGMA foreign_key_list({$table})")->getResult();
foreach ($query as $row) {
$obj = new stdClass();
$obj->constraint_name = $row->from . ' to ' . $row->table . '.' . $row->to;
$obj->table_name = $table;
$obj->foreign_table_name = $row->table;
$obj->sequence = $row->seq;
$retVal[] = $obj;
}
}
return $retVal;
}
/**
* Returns platform-specific SQL to disable foreign key checks.
*
* @return string
*/
protected function _disableForeignKeyChecks()
{
return 'PRAGMA foreign_keys = OFF';
}
/**
* Returns platform-specific SQL to enable foreign key checks.
*
* @return string
*/
protected function _enableForeignKeyChecks()
{
return 'PRAGMA foreign_keys = ON';
}
/**
* Returns the last error code and message.
* Must return this format: ['code' => string|int, 'message' => string]
* intval(code) === 0 means "no error".
*
* @return array<string, int|string>
*/
public function error(): array
{
return [
'code' => $this->connID->lastErrorCode(),
'message' => $this->connID->lastErrorMsg(),
];
}
/**
* Insert ID
*/
public function insertID(): int
{
return $this->connID->lastInsertRowID();
}
/**
* Begin Transaction
*/
protected function _transBegin(): bool
{
return $this->connID->exec('BEGIN TRANSACTION');
}
/**
* Commit Transaction
*/
protected function _transCommit(): bool
{
return $this->connID->exec('END TRANSACTION');
}
/**
* Rollback Transaction
*/
protected function _transRollback(): bool
{
return $this->connID->exec('ROLLBACK');
}
/**
* Checks to see if the current install supports Foreign Keys
* and has them enabled.
*/
public function supportsForeignKeys(): bool
{
$result = $this->simpleQuery('PRAGMA foreign_keys');
return (bool) $result;
}
}

View File

@@ -0,0 +1,254 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Forge as BaseForge;
/**
* Forge for SQLite3
*/
class Forge extends BaseForge
{
/**
* DROP INDEX statement
*
* @var string
*/
protected $dropIndexStr = 'DROP INDEX %s';
/**
* @var Connection
*/
protected $db;
/**
* UNSIGNED support
*
* @var array|bool
*/
protected $_unsigned = false;
/**
* NULL value representation in CREATE/ALTER TABLE statements
*
* @var string
*
* @internal
*/
protected $null = 'NULL';
/**
* Constructor.
*/
public function __construct(BaseConnection $db)
{
parent::__construct($db);
if (version_compare($this->db->getVersion(), '3.3', '<')) {
$this->createTableIfStr = false;
$this->dropTableIfStr = false;
}
}
/**
* Create database
*
* @param bool $ifNotExists Whether to add IF NOT EXISTS condition
*/
public function createDatabase(string $dbName, bool $ifNotExists = false): bool
{
// In SQLite, a database is created when you connect to the database.
// We'll return TRUE so that an error isn't generated.
return true;
}
/**
* Drop database
*
* @throws DatabaseException
*/
public function dropDatabase(string $dbName): bool
{
// In SQLite, a database is dropped when we delete a file
if (! is_file($dbName)) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unable to drop the specified database.');
}
return false;
}
// We need to close the pseudo-connection first
$this->db->close();
if (! @unlink($dbName)) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unable to drop the specified database.');
}
return false;
}
if (! empty($this->db->dataCache['db_names'])) {
$key = array_search(strtolower($dbName), array_map('strtolower', $this->db->dataCache['db_names']), true);
if ($key !== false) {
unset($this->db->dataCache['db_names'][$key]);
}
}
return true;
}
/**
* @param mixed $field
*
* @return array|string|null
*/
protected function _alterTable(string $alterType, string $table, $field)
{
switch ($alterType) {
case 'DROP':
$sqlTable = new Table($this->db, $this);
$sqlTable->fromTable($table)
->dropColumn($field)
->run();
return '';
case 'CHANGE':
(new Table($this->db, $this))
->fromTable($table)
->modifyColumn($field)
->run();
return null;
default:
return parent::_alterTable($alterType, $table, $field);
}
}
/**
* Process column
*/
protected function _processColumn(array $field): string
{
if ($field['type'] === 'TEXT' && strpos($field['length'], "('") === 0) {
$field['type'] .= ' CHECK(' . $this->db->escapeIdentifiers($field['name'])
. ' IN ' . $field['length'] . ')';
}
return $this->db->escapeIdentifiers($field['name'])
. ' ' . $field['type']
. $field['auto_increment']
. $field['null']
. $field['unique']
. $field['default'];
}
/**
* Process indexes
*/
protected function _processIndexes(string $table): array
{
$sqls = [];
for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
$this->keys[$i] = (array) $this->keys[$i];
for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) {
if (! isset($this->fields[$this->keys[$i][$i2]])) {
unset($this->keys[$i][$i2]);
}
}
if (count($this->keys[$i]) <= 0) {
continue;
}
if (in_array($i, $this->uniqueKeys, true)) {
$sqls[] = 'CREATE UNIQUE INDEX ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
. ' ON ' . $this->db->escapeIdentifiers($table)
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
continue;
}
$sqls[] = 'CREATE INDEX ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
. ' ON ' . $this->db->escapeIdentifiers($table)
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
}
return $sqls;
}
/**
* Field attribute TYPE
*
* Performs a data type mapping between different databases.
*/
protected function _attributeType(array &$attributes)
{
switch (strtoupper($attributes['TYPE'])) {
case 'ENUM':
case 'SET':
$attributes['TYPE'] = 'TEXT';
break;
case 'BOOLEAN':
$attributes['TYPE'] = 'INT';
break;
default:
break;
}
}
/**
* Field attribute AUTO_INCREMENT
*/
protected function _attributeAutoIncrement(array &$attributes, array &$field)
{
if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true
&& stripos($field['type'], 'int') !== false) {
$field['type'] = 'INTEGER PRIMARY KEY';
$field['default'] = '';
$field['null'] = '';
$field['unique'] = '';
$field['auto_increment'] = ' AUTOINCREMENT';
$this->primaryKeys = [];
}
}
/**
* Foreign Key Drop
*
* @throws DatabaseException
*/
public function dropForeignKey(string $table, string $foreignName): bool
{
// If this version of SQLite doesn't support it, we're done here
if ($this->db->supportsForeignKeys() !== true) {
return true;
}
// Otherwise we have to copy the table and recreate
// without the foreign key being involved now
$sqlTable = new Table($this->db, $this);
return $sqlTable->fromTable($this->db->DBPrefix . $table)
->dropForeignKey($foreignName)
->run();
}
}

View File

@@ -0,0 +1,91 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use BadMethodCallException;
use CodeIgniter\Database\BasePreparedQuery;
/**
* Prepared query for SQLite3
*/
class PreparedQuery extends BasePreparedQuery
{
/**
* The SQLite3Result resource, or false.
*
* @var bool|Result
*/
protected $result;
/**
* Prepares the query against the database, and saves the connection
* info necessary to execute the query later.
*
* NOTE: This version is based on SQL code. Child classes should
* override this method.
*
* @param array $options Passed to the connection's prepare statement.
* Unused in the MySQLi driver.
*
* @return $this
*/
public function _prepare(string $sql, array $options = [])
{
if (! ($this->statement = $this->db->connID->prepare($sql))) {
$this->errorCode = $this->db->connID->lastErrorCode();
$this->errorString = $this->db->connID->lastErrorMsg();
}
return $this;
}
/**
* Takes a new set of data and runs it against the currently
* prepared query. Upon success, will return a Results object.
*
* @todo finalize()
*/
public function _execute(array $data): bool
{
if (! isset($this->statement)) {
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
}
foreach ($data as $key => $item) {
// Determine the type string
if (is_int($item)) {
$bindType = SQLITE3_INTEGER;
} elseif (is_float($item)) {
$bindType = SQLITE3_FLOAT;
} else {
$bindType = SQLITE3_TEXT;
}
// Bind it
$this->statement->bindValue($key + 1, $item, $bindType);
}
$this->result = $this->statement->execute();
return $this->result !== false;
}
/**
* Returns the result object for the prepared query.
*
* @return mixed
*/
public function _getResult()
{
return $this->result;
}
}

View File

@@ -0,0 +1,152 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use Closure;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Entity\Entity;
use stdClass;
/**
* Result for SQLite3
*/
class Result extends BaseResult
{
/**
* Gets the number of fields in the result set.
*/
public function getFieldCount(): int
{
return $this->resultID->numColumns();
}
/**
* Generates an array of column names in the result set.
*/
public function getFieldNames(): array
{
$fieldNames = [];
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
$fieldNames[] = $this->resultID->columnName($i);
}
return $fieldNames;
}
/**
* Generates an array of objects representing field meta-data.
*/
public function getFieldData(): array
{
static $dataTypes = [
SQLITE3_INTEGER => 'integer',
SQLITE3_FLOAT => 'float',
SQLITE3_TEXT => 'text',
SQLITE3_BLOB => 'blob',
SQLITE3_NULL => 'null',
];
$retVal = [];
$this->resultID->fetchArray(SQLITE3_NUM);
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
$retVal[$i] = new stdClass();
$retVal[$i]->name = $this->resultID->columnName($i);
$type = $this->resultID->columnType($i);
$retVal[$i]->type = $type;
$retVal[$i]->type_name = $dataTypes[$type] ?? null;
$retVal[$i]->max_length = null;
$retVal[$i]->length = null;
}
$this->resultID->reset();
return $retVal;
}
/**
* Frees the current result.
*/
public function freeResult()
{
if (is_object($this->resultID)) {
$this->resultID->finalize();
$this->resultID = false;
}
}
/**
* Moves the internal pointer to the desired offset. This is called
* internally before fetching results to make sure the result set
* starts at zero.
*
* @throws DatabaseException
*
* @return mixed
*/
public function dataSeek(int $n = 0)
{
if ($n !== 0) {
throw new DatabaseException('SQLite3 doesn\'t support seeking to other offset.');
}
return $this->resultID->reset();
}
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return mixed
*/
protected function fetchAssoc()
{
return $this->resultID->fetchArray(SQLITE3_ASSOC);
}
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @return bool|object
*/
protected function fetchObject(string $className = 'stdClass')
{
// No native support for fetching rows as objects
if (($row = $this->fetchAssoc()) === false) {
return false;
}
if ($className === 'stdClass') {
return (object) $row;
}
$classObj = new $className();
if (is_subclass_of($className, Entity::class)) {
return $classObj->setAttributes($row);
}
$classSet = Closure::bind(function ($key, $value) {
$this->{$key} = $value;
}, $classObj, $className);
foreach (array_keys($row) as $key) {
$classSet($key, $row[$key]);
}
return $classObj;
}
}

View File

@@ -0,0 +1,363 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use CodeIgniter\Database\Exceptions\DataException;
/**
* Class Table
*
* Provides missing features for altering tables that are common
* in other supported databases, but are missing from SQLite.
* These are needed in order to support migrations during testing
* when another database is used as the primary engine, but
* SQLite in memory databases are used for faster test execution.
*/
class Table
{
/**
* All of the fields this table represents.
*
* @var array
*/
protected $fields = [];
/**
* All of the unique/primary keys in the table.
*
* @var array
*/
protected $keys = [];
/**
* All of the foreign keys in the table.
*
* @var array
*/
protected $foreignKeys = [];
/**
* The name of the table we're working with.
*
* @var string
*/
protected $tableName;
/**
* The name of the table, with database prefix
*
* @var string
*/
protected $prefixedTableName;
/**
* Database connection.
*
* @var Connection
*/
protected $db;
/**
* Handle to our forge.
*
* @var Forge
*/
protected $forge;
/**
* Table constructor.
*/
public function __construct(Connection $db, Forge $forge)
{
$this->db = $db;
$this->forge = $forge;
}
/**
* Reads an existing database table and
* collects all of the information needed to
* recreate this table.
*
* @return Table
*/
public function fromTable(string $table)
{
$this->prefixedTableName = $table;
$prefix = $this->db->DBPrefix;
if (! empty($prefix) && strpos($table, $prefix) === 0) {
$table = substr($table, strlen($prefix));
}
if (! $this->db->tableExists($this->prefixedTableName)) {
throw DataException::forTableNotFound($this->prefixedTableName);
}
$this->tableName = $table;
$this->fields = $this->formatFields($this->db->getFieldData($table));
$this->keys = array_merge($this->keys, $this->formatKeys($this->db->getIndexData($table)));
$this->foreignKeys = $this->db->getForeignKeyData($table);
return $this;
}
/**
* Called after `fromTable` and any actions, like `dropColumn`, etc,
* to finalize the action. It creates a temp table, creates the new
* table with modifications, and copies the data over to the new table.
* Resets the connection dataCache to be sure changes are collected.
*/
public function run(): bool
{
$this->db->query('PRAGMA foreign_keys = OFF');
$this->db->transStart();
$this->forge->renameTable($this->tableName, "temp_{$this->tableName}");
$this->forge->reset();
$this->createTable();
$this->copyData();
$this->forge->dropTable("temp_{$this->tableName}");
$success = $this->db->transComplete();
$this->db->query('PRAGMA foreign_keys = ON');
$this->db->resetDataCache();
return $success;
}
/**
* Drops columns from the table.
*
* @param array|string $columns
*
* @return Table
*/
public function dropColumn($columns)
{
if (is_string($columns)) {
$columns = explode(',', $columns);
}
foreach ($columns as $column) {
$column = trim($column);
if (isset($this->fields[$column])) {
unset($this->fields[$column]);
}
}
return $this;
}
/**
* Modifies a field, including changing data type,
* renaming, etc.
*
* @return Table
*/
public function modifyColumn(array $field)
{
$field = $field[0];
$oldName = $field['name'];
unset($field['name']);
$this->fields[$oldName] = $field;
return $this;
}
/**
* Drops a foreign key from this table so that
* it won't be recreated in the future.
*
* @return Table
*/
public function dropForeignKey(string $column)
{
if (empty($this->foreignKeys)) {
return $this;
}
for ($i = 0; $i < count($this->foreignKeys); $i++) {
if ($this->foreignKeys[$i]->table_name !== $this->tableName) {
continue;
}
// The column name should be the first thing in the constraint name
if (strpos($this->foreignKeys[$i]->constraint_name, $column) !== 0) {
continue;
}
unset($this->foreignKeys[$i]);
}
return $this;
}
/**
* Creates the new table based on our current fields.
*
* @return mixed
*/
protected function createTable()
{
$this->dropIndexes();
$this->db->resetDataCache();
// Handle any modified columns.
$fields = [];
foreach ($this->fields as $name => $field) {
if (isset($field['new_name'])) {
$fields[$field['new_name']] = $field;
continue;
}
$fields[$name] = $field;
}
$this->forge->addField($fields);
// Unique/Index keys
if (is_array($this->keys)) {
foreach ($this->keys as $key) {
switch ($key['type']) {
case 'primary':
$this->forge->addPrimaryKey($key['fields']);
break;
case 'unique':
$this->forge->addUniqueKey($key['fields']);
break;
case 'index':
$this->forge->addKey($key['fields']);
break;
}
}
}
return $this->forge->createTable($this->tableName);
}
/**
* Copies data from our old table to the new one,
* taking care map data correctly based on any columns
* that have been renamed.
*/
protected function copyData()
{
$exFields = [];
$newFields = [];
foreach ($this->fields as $name => $details) {
$newFields[] = $details['new_name'] ?? $name;
$exFields[] = $name;
}
$exFields = implode(', ', $exFields);
$newFields = implode(', ', $newFields);
$this->db->query("INSERT INTO {$this->prefixedTableName}({$newFields}) SELECT {$exFields} FROM {$this->db->DBPrefix}temp_{$this->tableName}");
}
/**
* Converts fields retrieved from the database to
* the format needed for creating fields with Forge.
*
* @param array|bool $fields
*
* @return mixed
*/
protected function formatFields($fields)
{
if (! is_array($fields)) {
return $fields;
}
$return = [];
foreach ($fields as $field) {
$return[$field->name] = [
'type' => $field->type,
'default' => $field->default,
'null' => $field->nullable,
];
if ($field->primary_key) {
$this->keys[$field->name] = [
'fields' => [$field->name],
'type' => 'primary',
];
}
}
return $return;
}
/**
* Converts keys retrieved from the database to
* the format needed to create later.
*
* @param mixed $keys
*
* @return mixed
*/
protected function formatKeys($keys)
{
if (! is_array($keys)) {
return $keys;
}
$return = [];
foreach ($keys as $name => $key) {
$return[$name] = [
'fields' => $key->fields,
'type' => 'index',
];
}
return $return;
}
/**
* Attempts to drop all indexes and constraints
* from the database for this table.
*/
protected function dropIndexes()
{
if (! is_array($this->keys) || $this->keys === []) {
return;
}
foreach ($this->keys as $name => $key) {
if ($key['type'] === 'primary' || $key['type'] === 'unique') {
continue;
}
$this->db->query("DROP INDEX IF EXISTS '{$name}'");
}
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database\SQLite3;
use CodeIgniter\Database\BaseUtils;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Utils for SQLite3
*/
class Utils extends BaseUtils
{
/**
* OPTIMIZE TABLE statement
*
* @var string
*/
protected $optimizeTable = 'REINDEX %s';
/**
* Platform dependent version of the backup function.
*
* @return mixed
*/
public function _backup(?array $prefs = null)
{
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
}

193
system/Database/Seeder.php Normal file
View File

@@ -0,0 +1,193 @@
<?php
/**
* This file is part of CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <admin@codeigniter.com>
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace CodeIgniter\Database;
use CodeIgniter\CLI\CLI;
use Config\Database;
use Faker\Factory;
use Faker\Generator;
use InvalidArgumentException;
/**
* Class Seeder
*/
class Seeder
{
/**
* The name of the database group to use.
*
* @var string
*/
protected $DBGroup;
/**
* Where we can find the Seed files.
*
* @var string
*/
protected $seedPath;
/**
* An instance of the main Database configuration
*
* @var Database
*/
protected $config;
/**
* Database Connection instance
*
* @var BaseConnection
*/
protected $db;
/**
* Database Forge instance.
*
* @var Forge
*/
protected $forge;
/**
* If true, will not display CLI messages.
*
* @var bool
*/
protected $silent = false;
/**
* Faker Generator instance.
*
* @var Generator|null
*
* @deprecated
*/
private static $faker;
/**
* Seeder constructor.
*/
public function __construct(Database $config, ?BaseConnection $db = null)
{
$this->seedPath = $config->filesPath ?? APPPATH . 'Database/';
if (empty($this->seedPath)) {
throw new InvalidArgumentException('Invalid filesPath set in the Config\Database.');
}
$this->seedPath = rtrim($this->seedPath, '\\/') . '/Seeds/';
if (! is_dir($this->seedPath)) {
throw new InvalidArgumentException('Unable to locate the seeds directory. Please check Config\Database::filesPath');
}
$this->config = &$config;
$db = $db ?? Database::connect($this->DBGroup);
$this->db = &$db;
$this->forge = Database::forge($this->DBGroup);
}
/**
* Gets the Faker Generator instance.
*
* @deprecated
*/
public static function faker(): ?Generator
{
if (self::$faker === null && class_exists(Factory::class)) {
self::$faker = Factory::create();
}
return self::$faker;
}
/**
* Loads the specified seeder and runs it.
*
* @throws InvalidArgumentException
*/
public function call(string $class)
{
$class = trim($class);
if ($class === '') {
throw new InvalidArgumentException('No seeder was specified.');
}
if (strpos($class, '\\') === false) {
$path = $this->seedPath . str_replace('.php', '', $class) . '.php';
if (! is_file($path)) {
throw new InvalidArgumentException('The specified seeder is not a valid file: ' . $path);
}
// Assume the class has the correct namespace
// @codeCoverageIgnoreStart
$class = APP_NAMESPACE . '\Database\Seeds\\' . $class;
if (! class_exists($class, false)) {
require_once $path;
}
// @codeCoverageIgnoreEnd
}
/** @var Seeder $seeder */
$seeder = new $class($this->config);
$seeder->setSilent($this->silent)->run();
unset($seeder);
if (is_cli() && ! $this->silent) {
CLI::write("Seeded: {$class}", 'green');
}
}
/**
* Sets the location of the directory that seed files can be located in.
*
* @return $this
*/
public function setPath(string $path)
{
$this->seedPath = rtrim($path, '\\/') . '/';
return $this;
}
/**
* Sets the silent treatment.
*
* @return $this
*/
public function setSilent(bool $silent)
{
$this->silent = $silent;
return $this;
}
/**
* Run the database seeds. This is where the magic happens.
*
* Child classes must implement this method and take care
* of inserting their data here.
*
* @return mixed
*
* @codeCoverageIgnore
*/
public function run()
{
}
}