added system

This commit is contained in:
dev-chiefworks
2024-09-01 23:43:50 -04:00
parent df3d016913
commit 558486d396
593 changed files with 114082 additions and 125 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+262
View File
@@ -0,0 +1,262 @@
<?php
declare(strict_types=1);
/**
* 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 ArgumentCountError;
use BadMethodCallException;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Events\Events;
use ErrorException;
/**
* @template TConnection
* @template TStatement
* @template TResult
*
* @implements PreparedQueryInterface<TConnection, TStatement, TResult>
*/
abstract class BasePreparedQuery implements PreparedQueryInterface
{
/**
* The prepared statement itself.
*
* @var object|resource|null
* @phpstan-var TStatement|null
*/
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
* @phpstan-var BaseConnection<TConnection, TResult>
*/
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 $this
*/
public function prepare(string $sql, array $options = [], string $queryClass = Query::class)
{
// 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 $this
*/
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 bool|ResultInterface
* @phpstan-return bool|ResultInterface<TConnection, TResult>
*
* @throws DatabaseException
*/
public function execute(...$data)
{
// Execute the Query.
$startTime = microtime(true);
try {
$exception = null;
$result = $this->_execute($data);
} catch (ArgumentCountError|ErrorException $exception) {
$result = false;
}
// Update our query object
$query = clone $this->query;
$query->setBinds($data);
if ($result === false) {
$query->setDuration($startTime, $startTime);
// This will trigger a rollback if transactions are being used
if ($this->db->transDepth !== 0) {
$this->db->transStatus = false;
}
if ($this->db->DBDebug) {
// We call this function in order to roll-back queries
// if transactions are enabled. If we don't call this here
// the error message will trigger an exit, causing the
// transactions to remain in limbo.
while ($this->db->transDepth !== 0) {
$transDepth = $this->db->transDepth;
$this->db->transComplete();
if ($transDepth === $this->db->transDepth) {
log_message('error', 'Database: Failure during an automated transaction commit/rollback!');
break;
}
}
// Let others do something with this query.
Events::trigger('DBQuery', $query);
if ($exception !== null) {
throw new DatabaseException($exception->getMessage(), $exception->getCode(), $exception);
}
return false;
}
// Let others do something with this query.
Events::trigger('DBQuery', $query);
return false;
}
$query->setDuration($startTime);
// Let others do something with this query
Events::trigger('DBQuery', $query);
if ($this->db->isWriteType((string) $query)) {
return true;
}
// 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 object|resource|null
*/
abstract public function _getResult();
/**
* Explicitly closes the prepared statement.
*
* @throws BadMethodCallException
*/
public function close(): bool
{
if (! isset($this->statement)) {
throw new BadMethodCallException('Cannot call close on a non-existing prepared statement.');
}
try {
return $this->_close();
} finally {
$this->statement = null;
}
}
/**
* The database-dependent version of the close method.
*/
abstract protected function _close(): bool;
/**
* 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;
}
}
+545
View File
@@ -0,0 +1,545 @@
<?php
declare(strict_types=1);
/**
* 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;
use stdClass;
/**
* @template TConnection
* @template TResult
*
* @implements ResultInterface<TConnection, TResult>
*/
abstract class BaseResult implements ResultInterface
{
/**
* Connection ID
*
* @var object|resource
* @phpstan-var TConnection
*/
public $connID;
/**
* Result ID
*
* @var false|object|resource
* @phpstan-var false|TResult
*/
public $resultID;
/**
* Result Array
*
* @var list<array>
*/
public $resultArray = [];
/**
* Result Object
*
* @var list<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
* @phpstan-param TConnection $connID
* @phpstan-param TResult $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.
*
* @phpstan-param class-string $className
*
* @return array
*/
public function getCustomResultObject(string $className)
{
if (isset($this->customResultObject[$className])) {
return $this->customResultObject[$className];
}
if (! $this->isValidResultId()) {
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 ($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 (! $this->isValidResultId()) {
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.
*
* @return array<int, stdClass>
* @phpstan-return list<stdClass>
*/
public function getResultObject(): array
{
if ($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 (! $this->isValidResultId()) {
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 the row doesn't exist, returns null.
*
* @template T of object
*
* @param int|string $n The index of the results to return, or column name.
* @param string $type The type of result object. 'array', 'object' or class name.
* @phpstan-param class-string<T>|'array'|'object' $type
*
* @return array|float|int|object|stdClass|string|null
* @phpstan-return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)))
*/
public function getRow($n = 0, string $type = 'object')
{
// $n is a column name.
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 the row doesn't exist, returns null.
*
* @template T of object
*
* @param int $n The index of the results to return.
* @phpstan-param class-string<T> $className
*
* @return object|null
* @phpstan-return T|null
*/
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 array|null
*/
public function getRowArray(int $n = 0)
{
$result = $this->getResultArray();
if ($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 object|stdClass|null
*/
public function getRowObject(int $n = 0)
{
$result = $this->getResultObject();
if ($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 array|string $key
* @param array|object|stdClass|null $value
*
* @return void
*/
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 array|object|null
*/
public function getFirstRow(string $type = 'object')
{
$result = $this->getResult($type);
return ($result === []) ? null : $result[0];
}
/**
* Returns the "last" row of the current results.
*
* @return array|object|null
*/
public function getLastRow(string $type = 'object')
{
$result = $this->getResult($type);
return ($result === []) ? null : $result[count($result) - 1];
}
/**
* Returns the "next" row of the current results.
*
* @return array|object|null
*/
public function getNextRow(string $type = 'object')
{
$result = $this->getResult($type);
if ($result === []) {
return null;
}
return isset($result[$this->currentRow + 1]) ? $result[++$this->currentRow] : null;
}
/**
* Returns the "previous" row of the current results.
*
* @return array|object|null
*/
public function getPreviousRow(string $type = 'object')
{
$result = $this->getResult($type);
if ($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 array|object|null
*/
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());
}
private function isValidResultId(): bool
{
return is_resource($this->resultID) || is_object($this->resultID);
}
/**
* 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.
*
* @return void
*/
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 bool
*/
abstract public function dataSeek(int $n = 0);
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return array|false|null
*/
abstract protected function fetchAssoc();
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @return Entity|false|object|stdClass
*/
abstract protected function fetchObject(string $className = 'stdClass');
}
+328
View File
@@ -0,0 +1,328 @@
<?php
declare(strict_types=1);
/**
* 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
*
* @return array|bool
*
* @throws DatabaseException
*/
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
*
* @return bool
*
* @throws DatabaseException
*/
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
*
* @return mixed
*
* @throws DatabaseException
*/
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
*
* @return mixed
*
* @throws DatabaseException
*/
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,
(string) $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((string) $val) : '';
$xml .= $tab . $tab . '<' . $key . '>' . $val . '</' . $key . '>' . $newline;
}
$xml .= $tab . '</' . $element . '>' . $newline;
}
return $xml . '</' . $root . '>' . $newline;
}
/**
* Database Backup
*
* @param array|string $params
*
* @return false|never|string
*
* @throws DatabaseException
*/
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);
}
// @TODO gzencode() requires `ext-zlib`, but _backup() is not implemented in all databases.
return gzencode($this->_backup($prefs));
}
/**
* Platform dependent version of the backup function.
*
* @return false|never|string
*/
abstract public function _backup(?array $prefs = null);
}
+153
View File
@@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
/**
* 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 Config\Database as DbConfig;
use InvalidArgumentException;
/**
* Class Config
*
* @see \CodeIgniter\Database\ConfigTest
*/
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;
/**
* Returns the database connection
*
* @param array|BaseConnection|non-empty-string|null $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));
} else {
$dbConfig = config(DbConfig::class);
if ($group === null) {
$group = (ENVIRONMENT === 'testing') ? 'tests' : $dbConfig->defaultGroup;
}
assert(is_string($group));
if (! isset($dbConfig->{$group})) {
throw new InvalidArgumentException('"' . $group . '" is not a valid database connection group.');
}
$config = $dbConfig->{$group};
}
if ($getShared && isset(static::$instances[$group])) {
return static::$instances[$group];
}
static::ensureFactory();
$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.
*
* @param non-empty-string|null $group
*
* @return Seeder
*/
public static function seeder(?string $group = null)
{
$config = config(DbConfig::class);
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();
}
}
+168
View File
@@ -0,0 +1,168 @@
<?php
declare(strict_types=1);
/**
* 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;
/**
* @template TConnection
* @template TResult
*
* @property false|object|resource $connID
* @property-read string $DBDriver
*/
interface ConnectionInterface
{
/**
* Initializes the database connection/settings.
*
* @return void
*/
public function initialize();
/**
* Connect to the database.
*
* @return false|object|resource
* @phpstan-return false|TConnection
*/
public function connect(bool $persistent = false);
/**
* Create a persistent database connection.
*
* @return false|object|resource
* @phpstan-return false|TConnection
*/
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 void
*/
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 false|object|resource
* @phpstan-return false|TConnection
*/
public function getConnection(?string $alias = null);
/**
* Select a specific database table to use.
*
* @return bool
*/
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 array|string|null $binds
*
* @return BaseResult|bool|Query
* @phpstan-return BaseResult<TConnection, TResult>|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 false|object|resource
* @phpstan-return false|TResult
*/
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 Query
*/
public function getLastQuery();
/**
* "Smart" Escaping
*
* Escapes data based on type.
* Sets boolean and null types.
*
* @param array|bool|float|int|object|string|null $str
*
* @return array|float|int|string
* @phpstan-return ($str is array ? array : float|int|string)
*/
public function escape($str);
/**
* Allows for custom calls to the database engine that are not
* supported through our database layer.
*
* @param array ...$params
*
* @return array|bool|float|int|object|resource|string|null
*/
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;
}
+141
View File
@@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
/**
* 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 Database Connection.
*/
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 creates a Database Connection instance.
*
* @return BaseConnection
*
* @throws InvalidArgumentException
*/
public function load(array $params = [], string $alias = '')
{
if ($alias === '') {
throw new InvalidArgumentException('You must supply the parameter: alias.');
}
if (! empty($params['DSN']) && str_contains($params['DSN'], '://')) {
$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): Forge
{
if (! $db->connID) {
$db->initialize();
}
return $this->initDriver($db->DBDriver, 'Forge', $db);
}
/**
* Creates an instance of Utils for the current database type.
*/
public function loadUtils(ConnectionInterface $db): BaseUtils
{
if (! $db->connID) {
$db->initialize();
}
return $this->initDriver($db->DBDriver, 'Utils', $db);
}
/**
* Parses 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 (isset($dsn['query']) && ($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);
}
/**
* Creates a database object.
*
* @param string $driver Driver name. FQCN can be used.
* @param string $class 'Connection'|'Forge'|'Utils'
* @param array|object $argument The constructor parameter.
*
* @return BaseConnection|BaseUtils|Forge
*/
protected function initDriver(string $driver, string $class, $argument): object
{
$classname = (! str_contains($driver, '\\'))
? "CodeIgniter\\Database\\{$driver}\\{$class}"
: $driver . '\\' . $class;
return new $classname($argument);
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
/**
* 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'));
}
}
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
/**
* 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\HasExitCodeInterface;
use Error;
class DatabaseException extends Error implements ExceptionInterface, HasExitCodeInterface
{
public function getExitCode(): int
{
return EXIT_DATABASE;
}
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
/**
* 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
{
}
File diff suppressed because it is too large Load Diff
+74
View File
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* 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|null
*/
protected $DBGroup;
/**
* Database Connection instance
*
* @var ConnectionInterface
*/
protected $db;
/**
* Database Forge instance.
*
* @var Forge
*/
protected $forge;
public function __construct(?Forge $forge = null)
{
if (isset($this->DBGroup)) {
$this->forge = Database::forge($this->DBGroup);
} elseif ($forge !== null) {
$this->forge = $forge;
} else {
$this->forge = Database::forge(config(Database::class)->defaultGroup);
}
$this->db = $this->forge->getConnection();
}
/**
* Returns the database group name this migration uses.
*/
public function getDBGroup(): ?string
{
return $this->DBGroup;
}
/**
* Perform a migration step.
*/
abstract public function up();
/**
* Revert a migration step.
*/
abstract public function down();
}
+871
View File
@@ -0,0 +1,871 @@
<?php
declare(strict_types=1);
/**
* 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 CodeIgniter\I18n\Time;
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.
* `null` is all namespaces.
*
* @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 = '/\A(\d{4}[_-]?\d{2}[_-]?\d{2}[_-]?\d{6})_(\w+)\z/';
/**
* 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;
/**
* The migration can manage multiple databases. So it should always use the
* default DB group so that it creates the `migrations` table in the default
* DB group. Therefore, passing $db is for testing purposes only.
*
* @param array|ConnectionInterface|string|null $db DB group. For testing purposes only.
*
* @throws ConfigException
*/
public function __construct(MigrationsConfig $config, $db = null)
{
$this->enabled = $config->enabled ?? false;
$this->table = $config->table ?? 'migrations';
$this->namespace = APP_NAMESPACE;
// Even if a DB connection is passed, since it is a test,
// it is assumed to use the default group name
$this->group = is_string($db) ? $db : config(Database::class)->defaultGroup;
$this->db = db_connect($db);
}
/**
* Locate and run all new migrations
*
* @return bool
*
* @throws ConfigException
* @throws RuntimeException
*/
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 ($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
* @param string|null $group Deprecated. The designation has no effect.
*
* @return bool True on success, FALSE on failure or no migrations are found
*
* @throws ConfigException
* @throws RuntimeException
*/
public function regress(int $targetBatch = 0, ?string $group = null)
{
if (! $this->enabled) {
throw ConfigException::forDisabledMigrations();
}
$this->ensureTable();
$batches = $this->getBatches();
if ($targetBatch < 0) {
$targetBatch = $batches[count($batches) - 1 + $targetBatch] ?? 0;
}
if ($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) {
if (ENVIRONMENT !== 'testing' && $namespace === 'Tests\Support') {
continue;
}
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, false, false);
} 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.
*
* @param string $path Full path to a valid migration file.
*
* @return false|object Returns the migration object, or false on failure
*/
protected function migrationFromFile(string $path, string $namespace)
{
if (! str_ends_with($path, '.php')) {
return false;
}
$filename = basename($path, '.php');
if (! preg_match($this->regex, $filename)) {
return false;
}
$locator = Services::locator(true);
$migration = new stdClass();
$migration->version = $this->getMigrationNumber($filename);
$migration->name = $this->getMigrationName($filename);
$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
*
* @param string $migration A migration filename w/o path.
*/
protected function getMigrationNumber(string $migration): string
{
preg_match($this->regex, $migration, $matches);
return count($matches) ? $matches[1] : '0';
}
/**
* Extracts the migration name from a filename
*
* Note: The migration name should be the classname, but maybe they are
* different.
*
* @param string $migration A migration filename w/o path.
*/
protected function getMigrationName(string $migration): string
{
preg_match($this->regex, $migration, $matches);
return count($matches) ? $matches[2] : '';
}
/**
* 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::now()->getTimestamp(),
'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 ($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 $migration === [] ? '0' : $migration[0]->version;
}
/**
* 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);
}
/** @var Migration $instance */
$instance = new $class(Database::forge($this->db));
$group = $instance->getDBGroup() ?? $this->group;
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;
}
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;
}
}
+147
View File
@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
/**
* 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;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\RawSql;
/**
* 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 ($this->QBJoin !== [] && count($this->QBFrom) > 1) {
return '(' . implode(', ', $this->QBFrom) . ')';
}
return implode(', ', $this->QBFrom);
}
/**
* Generates a platform-specific batch update string from the supplied data
*/
protected function _updateBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$constraints = $this->QBOptions['constraints'] ?? [];
if ($constraints === []) {
if ($this->db->DBDebug) {
throw new DatabaseException('You must specify a constraint to match on for batch updates.'); // @codeCoverageIgnore
}
return ''; // @codeCoverageIgnore
}
$updateFields = $this->QBOptions['updateFields'] ??
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
[];
$alias = $this->QBOptions['alias'] ?? '`_u`';
$sql = 'UPDATE ' . $this->compileIgnore('update') . $table . "\n";
$sql .= "INNER JOIN (\n{:_table_:}";
$sql .= ') ' . $alias . "\n";
$sql .= 'ON ' . implode(
' AND ',
array_map(
static fn ($key, $value) => (
($value instanceof RawSql && is_string($key))
?
$table . '.' . $key . ' = ' . $value
:
(
$value instanceof RawSql
?
$value
:
$table . '.' . $value . ' = ' . $alias . '.' . $value
)
),
array_keys($constraints),
$constraints
)
) . "\n";
$sql .= "SET\n";
$sql .= implode(
",\n",
array_map(
static fn ($key, $value) => $table . '.' . $key . ($value instanceof RawSql ?
' = ' . $value :
' = ' . $alias . '.' . $value),
array_keys($updateFields),
$updateFields
)
);
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = implode(
" UNION ALL\n",
array_map(
static fn ($value) => 'SELECT ' . implode(', ', array_map(
static fn ($key, $index) => $index . ' ' . $key,
$keys,
$value
)),
$values
)
) . "\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
}
+634
View File
@@ -0,0 +1,634 @@
<?php
declare(strict_types=1);
/**
* 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_result;
use mysqli_sql_exception;
use stdClass;
use Throwable;
/**
* Connection for MySQLi
*
* @extends BaseConnection<mysqli, mysqli_result>
*/
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 false|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;
/**
* Use MYSQLI_OPT_INT_AND_FLOAT_NATIVE
*
* @var bool
*/
public $numberNative = false;
/**
* Connect to the database.
*
* @return false|mysqli
*
* @throws DatabaseException
*/
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 ($this->numberNative === true) {
$this->mysqli->options(MYSQLI_OPT_INT_AND_FLOAT_NATIVE, 1);
}
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 ($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.
*
* @return void
*/
public function reconnect()
{
$this->close();
$this->initialize();
}
/**
* Close the database connection.
*
* @return void
*/
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 false|mysqli_result;
*/
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', (string) $e);
if ($this->DBDebug) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $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 list<string>|string $str Input string
*
* @return list<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().
*
* @param string|null $tableName If $tableName is provided will return only this table if exists.
*/
protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
{
$sql = 'SHOW TABLES FROM ' . $this->escapeIdentifier($this->database);
if ($tableName !== null) {
return $sql . ' LIKE ' . $this->escape($tableName);
}
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
*
* @return list<stdClass>
*
* @throws DatabaseException
*/
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
*
* @return array<string, stdClass>
*
* @throws DatabaseException
* @throws LogicException
*/
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
*
* @return array<string, stdClass>
*
* @throws DatabaseException
*/
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,
rc.DELETE_RULE,
rc.UPDATE_RULE,
rc.MATCH_OPTION
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();
$indexes = [];
foreach ($query as $row) {
$indexes[$row->CONSTRAINT_NAME]['constraint_name'] = $row->CONSTRAINT_NAME;
$indexes[$row->CONSTRAINT_NAME]['table_name'] = $row->TABLE_NAME;
$indexes[$row->CONSTRAINT_NAME]['column_name'][] = $row->COLUMN_NAME;
$indexes[$row->CONSTRAINT_NAME]['foreign_table_name'] = $row->REFERENCED_TABLE_NAME;
$indexes[$row->CONSTRAINT_NAME]['foreign_column_name'][] = $row->REFERENCED_COLUMN_NAME;
$indexes[$row->CONSTRAINT_NAME]['on_delete'] = $row->DELETE_RULE;
$indexes[$row->CONSTRAINT_NAME]['on_update'] = $row->UPDATE_RULE;
$indexes[$row->CONSTRAINT_NAME]['match'] = $row->MATCH_OPTION;
}
return $this->foreignKeyDataToObjects($indexes);
}
/**
* 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;
}
}
+266
View File
@@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
/**
* 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 ($this->db->charset !== '' && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) {
$sql .= ' DEFAULT CHARACTER SET = ' . $this->db->escapeString($this->db->charset);
}
if ($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 array|string $processedFields Processed column definitions
* or column names to DROP
*
* @return list<string>|string SQL string
* @phpstan-return ($alterType is 'DROP' ? string : list<string>)
*/
protected function _alterTable(string $alterType, string $table, $processedFields)
{
if ($alterType === 'DROP') {
return parent::_alterTable($alterType, $table, $processedFields);
}
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
foreach ($processedFields as $i => $field) {
if ($field['_literal'] !== false) {
$processedFields[$i] = ($alterType === 'ADD') ? "\n\tADD " . $field['_literal'] : "\n\tMODIFY " . $field['_literal'];
} else {
if ($alterType === 'ADD') {
$processedFields[$i]['_literal'] = "\n\tADD ";
} else {
$processedFields[$i]['_literal'] = empty($field['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE ";
}
$processedFields[$i] = $processedFields[$i]['_literal'] . $this->_processColumn($processedFields[$i]);
}
}
return [$sql . implode(',', $processedFields)];
}
/**
* Process column
*/
protected function _processColumn(array $processedField): string
{
$extraClause = isset($processedField['after']) ? ' AFTER ' . $this->db->escapeIdentifiers($processedField['after']) : '';
if (empty($extraClause) && isset($processedField['first']) && $processedField['first'] === true) {
$extraClause = ' FIRST';
}
return $this->db->escapeIdentifiers($processedField['name'])
. (empty($processedField['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($processedField['new_name']))
. ' ' . $processedField['type'] . $processedField['length']
. $processedField['unsigned']
. $processedField['null']
. $processedField['default']
. $processedField['auto_increment']
. $processedField['unique']
. (empty($processedField['comment']) ? '' : ' COMMENT ' . $processedField['comment'])
. $extraClause;
}
/**
* Generates SQL to add indexes
*
* @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE
*/
protected function _processIndexes(string $table, bool $asQuery = false): array
{
$sqls = [''];
$index = 0;
for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
$index = $i;
if ($asQuery === false) {
$index = 0;
}
if (isset($this->keys[$i]['fields'])) {
for ($i2 = 0, $c2 = count($this->keys[$i]['fields']); $i2 < $c2; $i2++) {
if (! isset($this->fields[$this->keys[$i]['fields'][$i2]])) {
unset($this->keys[$i]['fields'][$i2]);
continue;
}
}
}
if (! is_array($this->keys[$i]['fields'])) {
$this->keys[$i]['fields'] = [$this->keys[$i]['fields']];
}
$unique = in_array($i, $this->uniqueKeys, true) ? 'UNIQUE ' : '';
$keyName = $this->db->escapeIdentifiers(($this->keys[$i]['keyName'] === '') ?
implode('_', $this->keys[$i]['fields']) :
$this->keys[$i]['keyName']);
if ($asQuery === true) {
$sqls[$index] = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table) . " ADD {$unique}KEY "
. $keyName
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ')';
} else {
$sqls[$index] .= ",\n\t{$unique}KEY " . $keyName
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ')';
}
}
$this->keys = [];
return $sqls;
}
/**
* Drop Key
*/
public function dropKey(string $table, string $keyName, bool $prefixKeyName = true): bool
{
$sql = sprintf(
$this->dropIndexStr,
$this->db->escapeIdentifiers($keyName),
$this->db->escapeIdentifiers($this->db->DBPrefix . $table),
);
return $this->db->query($sql);
}
/**
* Drop Primary Key
*/
public function dropPrimaryKey(string $table, string $keyName = ''): bool
{
$sql = sprintf(
'ALTER TABLE %s DROP PRIMARY KEY',
$this->db->escapeIdentifiers($this->db->DBPrefix . $table)
);
return $this->db->query($sql);
}
}
+114
View File
@@ -0,0 +1,114 @@
<?php
declare(strict_types=1);
/**
* 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;
use CodeIgniter\Database\Exceptions\DatabaseException;
use mysqli;
use mysqli_result;
use mysqli_sql_exception;
use mysqli_stmt;
/**
* Prepared query for MySQLi
*
* @extends BasePreparedQuery<mysqli, mysqli_stmt, mysqli_result>
*/
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.
*/
public function _prepare(string $sql, array $options = []): PreparedQuery
{
// 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;
if ($this->db->DBDebug) {
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
}
}
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);
try {
return $this->statement->execute();
} catch (mysqli_sql_exception $e) {
if ($this->db->DBDebug) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
}
return false;
}
}
/**
* Returns the result object for the prepared query or false on failure.
*
* @return false|mysqli_result
*/
public function _getResult()
{
return $this->statement->get_result();
}
/**
* Deallocate prepared statements.
*/
protected function _close(): bool
{
return $this->statement->close();
}
}
+170
View File
@@ -0,0 +1,170 @@
<?php
declare(strict_types=1);
/**
* 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 mysqli;
use mysqli_result;
use stdClass;
/**
* Result for MySQLi
*
* @extends BaseResult<mysqli, mysqli_result>
*/
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.
*
* @return void
*/
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 bool
*/
public function dataSeek(int $n = 0)
{
return $this->resultID->data_seek($n);
}
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return array|false|null
*/
protected function fetchAssoc()
{
return $this->resultID->fetch_assoc();
}
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @return Entity|false|object|stdClass
*/
protected function fetchObject(string $className = 'stdClass')
{
if (is_subclass_of($className, Entity::class)) {
return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($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;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* 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 never
*/
public function _backup(?array $prefs = null)
{
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
}
+526
View File
@@ -0,0 +1,526 @@
<?php
declare(strict_types=1);
/**
* 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\OCI8;
use CodeIgniter\Database\BaseBuilder;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\RawSql;
/**
* Builder for OCI8
*/
class Builder extends BaseBuilder
{
/**
* Identifier escape character
*
* @var string
*/
protected $escapeChar = '"';
/**
* ORDER BY random keyword
*
* @var array
*/
protected $randomKeyword = [
'"DBMS_RANDOM"."RANDOM"',
];
/**
* COUNT string
*
* @used-by CI_DB_driver::count_all()
* @used-by BaseBuilder::count_all_results()
*
* @var string
*/
protected $countString = 'SELECT COUNT(1) ';
/**
* Limit used flag
*
* If we use LIMIT, we'll add a field that will
* throw off num_fields later.
*
* @var bool
*/
protected $limitUsed = false;
/**
* A reference to the database connection.
*
* @var Connection
*/
protected $db;
/**
* Generates a platform-specific insert string from the supplied data.
*/
protected function _insertBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$insertKeys = implode(', ', $keys);
$hasPrimaryKey = in_array('PRIMARY', array_column($this->db->getIndexData($table), 'type'), true);
// ORA-00001 measures
$sql = 'INSERT' . ($hasPrimaryKey ? '' : ' ALL') . ' INTO ' . $table . ' (' . $insertKeys . ")\n{:_table_:}";
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = implode(
" FROM DUAL UNION ALL\n",
array_map(
static fn ($value) => 'SELECT ' . implode(', ', array_map(
static fn ($key, $index) => $index . ' ' . $key,
$keys,
$value
)),
$values
)
) . " FROM DUAL\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
/**
* Generates a platform-specific replace string from the supplied data
*/
protected function _replace(string $table, array $keys, array $values): string
{
$fieldNames = array_map(static fn ($columnName) => trim($columnName, '"'), $keys);
$uniqueIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames) {
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
return ($index->type === 'PRIMARY') && $hasAllFields;
});
$replaceableFields = array_filter($keys, static function ($columnName) use ($uniqueIndexes) {
foreach ($uniqueIndexes as $index) {
if (in_array(trim($columnName, '"'), $index->fields, true)) {
return false;
}
}
return true;
});
$sql = 'MERGE INTO ' . $table . "\n USING (SELECT ";
$sql .= implode(', ', array_map(static fn ($columnName, $value) => $value . ' ' . $columnName, $keys, $values));
$sql .= ' FROM DUAL) "_replace" ON ( ';
$onList = [];
$onList[] = '1 != 1';
foreach ($uniqueIndexes as $index) {
$onList[] = '(' . implode(' AND ', array_map(static fn ($columnName) => $table . '."' . $columnName . '" = "_replace"."' . $columnName . '"', $index->fields)) . ')';
}
$sql .= implode(' OR ', $onList) . ') WHEN MATCHED THEN UPDATE SET ';
$sql .= implode(', ', array_map(static fn ($columnName) => $columnName . ' = "_replace".' . $columnName, $replaceableFields));
$sql .= ' WHEN NOT MATCHED THEN INSERT (' . implode(', ', $replaceableFields) . ') VALUES ';
return $sql . (' (' . implode(', ', array_map(static fn ($columnName) => '"_replace".' . $columnName, $replaceableFields)) . ')');
}
/**
* 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 ' . $table;
}
/**
* Compiles a delete string and runs the query
*
* @param mixed $where
*
* @return mixed
*
* @throws DatabaseException
*/
public function delete($where = '', ?int $limit = null, bool $resetData = true)
{
if ($limit !== null && $limit !== 0) {
$this->QBLimit = $limit;
}
return parent::delete($where, null, $resetData);
}
/**
* Generates a platform-specific delete string from the supplied data
*/
protected function _delete(string $table): string
{
if ($this->QBLimit) {
$this->where('rownum <= ', $this->QBLimit, false);
$this->QBLimit = false;
}
return parent::_delete($table);
}
/**
* 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;
}
if ($this->QBLimit) {
$this->where('rownum <= ', $this->QBLimit, false);
}
return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr)
. $this->compileWhereHaving('QBWhere')
. $this->compileOrderBy();
}
/**
* Generates a platform-specific LIMIT clause.
*/
protected function _limit(string $sql, bool $offsetIgnore = false): string
{
$offset = (int) ($offsetIgnore === false ? $this->QBOffset : 0);
if (version_compare($this->db->getVersion(), '12.1', '>=')) {
// OFFSET-FETCH can be used only with the ORDER BY clause
if (empty($this->QBOrderBy)) {
$sql .= ' ORDER BY 1';
}
return $sql . ' OFFSET ' . $offset . ' ROWS FETCH NEXT ' . $this->QBLimit . ' ROWS ONLY';
}
$this->limitUsed = true;
$limitTemplateQuery = 'SELECT * FROM (SELECT INNER_QUERY.*, ROWNUM RNUM FROM (%s) INNER_QUERY WHERE ROWNUM < %d)' . ($offset !== 0 ? ' WHERE RNUM >= %d' : '');
return sprintf($limitTemplateQuery, $sql, $offset + $this->QBLimit + 1, $offset);
}
/**
* Resets the query builder values. Called by the get() function
*/
protected function resetSelect()
{
$this->limitUsed = false;
parent::resetSelect();
}
/**
* Generates a platform-specific batch update string from the supplied data
*/
protected function _updateBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$constraints = $this->QBOptions['constraints'] ?? [];
if ($constraints === []) {
if ($this->db->DBDebug) {
throw new DatabaseException('You must specify a constraint to match on for batch updates.');
}
return ''; // @codeCoverageIgnore
}
$updateFields = $this->QBOptions['updateFields'] ??
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
[];
$alias = $this->QBOptions['alias'] ?? '"_u"';
// Oracle doesn't support ignore on updates so we will use MERGE
$sql = 'MERGE INTO ' . $table . "\n";
$sql .= "USING (\n{:_table_:}";
$sql .= ') ' . $alias . "\n";
$sql .= 'ON (' . implode(
' AND ',
array_map(
static fn ($key, $value) => (
($value instanceof RawSql && is_string($key))
?
$table . '.' . $key . ' = ' . $value
:
(
$value instanceof RawSql
?
$value
:
$table . '.' . $value . ' = ' . $alias . '.' . $value
)
),
array_keys($constraints),
$constraints
)
) . ")\n";
$sql .= "WHEN MATCHED THEN UPDATE\n";
$sql .= "SET\n";
$sql .= implode(
",\n",
array_map(
static fn ($key, $value) => $table . '.' . $key . ($value instanceof RawSql ?
' = ' . $value :
' = ' . $alias . '.' . $value),
array_keys($updateFields),
$updateFields
)
);
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = implode(
" UNION ALL\n",
array_map(
static fn ($value) => 'SELECT ' . implode(', ', array_map(
static fn ($key, $index) => $index . ' ' . $key,
$keys,
$value
)) . ' FROM DUAL',
$values
)
) . "\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
/**
* Generates a platform-specific upsertBatch string from the supplied data
*
* @throws DatabaseException
*/
protected function _upsertBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$constraints = $this->QBOptions['constraints'] ?? [];
if (empty($constraints)) {
$fieldNames = array_map(static fn ($columnName) => trim($columnName, '"'), $keys);
$uniqueIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames) {
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
return ($index->type === 'PRIMARY' || $index->type === 'UNIQUE') && $hasAllFields;
});
// only take first index
foreach ($uniqueIndexes as $index) {
$constraints = $index->fields;
break;
}
$constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? [];
}
if (empty($constraints)) {
if ($this->db->DBDebug) {
throw new DatabaseException('No constraint found for upsert.');
}
return ''; // @codeCoverageIgnore
}
$alias = $this->QBOptions['alias'] ?? '"_upsert"';
$updateFields = $this->QBOptions['updateFields'] ?? $this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ?? [];
$sql = 'MERGE INTO ' . $table . "\nUSING (\n{:_table_:}";
$sql .= ") {$alias}\nON (";
$sql .= implode(
' AND ',
array_map(
static fn ($key, $value) => (
($value instanceof RawSql && is_string($key))
?
$table . '.' . $key . ' = ' . $value
:
(
$value instanceof RawSql
?
$value
:
$table . '.' . $value . ' = ' . $alias . '.' . $value
)
),
array_keys($constraints),
$constraints
)
) . ")\n";
$sql .= "WHEN MATCHED THEN UPDATE SET\n";
$sql .= implode(
",\n",
array_map(
static fn ($key, $value) => $key . ($value instanceof RawSql ?
" = {$value}" :
" = {$alias}.{$value}"),
array_keys($updateFields),
$updateFields
)
);
$sql .= "\nWHEN NOT MATCHED THEN INSERT (" . implode(', ', $keys) . ")\nVALUES ";
$sql .= (' ('
. implode(', ', array_map(static fn ($columnName) => "{$alias}.{$columnName}", $keys))
. ')');
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = implode(
" FROM DUAL UNION ALL\n",
array_map(
static fn ($value) => 'SELECT ' . implode(', ', array_map(
static fn ($key, $index) => $index . ' ' . $key,
$keys,
$value
)),
$values
)
) . " FROM DUAL\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
/**
* Generates a platform-specific batch update string from the supplied data
*/
protected function _deleteBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$constraints = $this->QBOptions['constraints'] ?? [];
if ($constraints === []) {
if ($this->db->DBDebug) {
throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore
}
return ''; // @codeCoverageIgnore
}
$alias = $this->QBOptions['alias'] ?? '_u';
$sql = 'DELETE ' . $table . "\n";
$sql .= "WHERE EXISTS (SELECT * FROM (\n{:_table_:}";
$sql .= ') ' . $alias . "\n";
$sql .= 'WHERE ' . implode(
' AND ',
array_map(
static fn ($key, $value) => (
$value instanceof RawSql ?
$value :
(
is_string($key) ?
$table . '.' . $key . ' = ' . $alias . '.' . $value :
$table . '.' . $value . ' = ' . $alias . '.' . $value
)
),
array_keys($constraints),
$constraints
)
);
// convert binds in where
foreach ($this->QBWhere as $key => $where) {
foreach ($this->binds as $field => $bind) {
$this->QBWhere[$key]['condition'] = str_replace(':' . $field . ':', $bind[0], $where['condition']);
}
}
$sql .= ' ' . str_replace(
'WHERE ',
'AND ',
$this->compileWhereHaving('QBWhere')
) . ')';
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = implode(
" FROM DUAL UNION ALL\n",
array_map(
static fn ($value) => 'SELECT ' . implode(', ', array_map(
static fn ($key, $index) => $index . ' ' . $key,
$keys,
$value
)),
$values
)
) . " FROM DUAL\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
/**
* Gets column names from a select query
*/
protected function fieldsFromQuery(string $sql): array
{
return $this->db->query('SELECT * FROM (' . $sql . ') "_u_" WHERE ROWNUM = 1')->getFieldNames();
}
}
+744
View File
@@ -0,0 +1,744 @@
<?php
declare(strict_types=1);
/**
* 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\OCI8;
use CodeIgniter\Database\BaseConnection;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Query;
use ErrorException;
use stdClass;
/**
* Connection for OCI8
*
* @extends BaseConnection<resource, resource>
*/
class Connection extends BaseConnection
{
/**
* Database driver
*
* @var string
*/
protected $DBDriver = 'OCI8';
/**
* Identifier escape character
*
* @var string
*/
public $escapeChar = '"';
/**
* List of reserved identifiers
*
* Identifiers that must NOT be escaped.
*
* @var array
*/
protected $reservedIdentifiers = [
'*',
'rownum',
];
protected $validDSNs = [
// TNS
'tns' => '/^\(DESCRIPTION=(\(.+\)){2,}\)$/',
// Easy Connect string (Oracle 10g+).
// https://docs.oracle.com/en/database/oracle/oracle-database/23/netag/configuring-naming-methods.html#GUID-36F3A17D-843C-490A-8A23-FB0FE005F8E8
// [//]host[:port][/[service_name][:server_type][/instance_name]]
'ec' => '/^
(\/\/)?
(\[)?[a-z0-9.:_-]+(\])? # Host or IP address
(:[1-9][0-9]{0,4})? # Port
(
(\/)
([a-z0-9.$_]+)? # Service name
(:[a-z]+)? # Server type
(\/[a-z0-9$_]+)? # Instance name
)?
$/ix',
// Instance name (defined in tnsnames.ora)
'in' => '/^[a-z0-9$_]+$/i',
];
/**
* Reset $stmtId flag
*
* Used by storedProcedure() to prevent execute() from
* re-setting the statement ID.
*/
protected $resetStmtId = true;
/**
* Statement ID
*
* @var resource
*/
protected $stmtId;
/**
* Commit mode flag
*
* @used-by PreparedQuery::_execute()
*
* @var int
*/
public $commitMode = OCI_COMMIT_ON_SUCCESS;
/**
* Cursor ID
*
* @var resource
*/
protected $cursorId;
/**
* Latest inserted table name.
*
* @used-by PreparedQuery::_execute()
*
* @var string|null
*/
public $lastInsertedTableName;
/**
* confirm DSN format.
*/
private function isValidDSN(): bool
{
if ($this->DSN === null || $this->DSN === '') {
return false;
}
foreach ($this->validDSNs as $regexp) {
if (preg_match($regexp, $this->DSN)) {
return true;
}
}
return false;
}
/**
* Connect to the database.
*
* @return false|resource
*/
public function connect(bool $persistent = false)
{
if (! $this->isValidDSN()) {
$this->buildDSN();
}
$func = $persistent ? 'oci_pconnect' : 'oci_connect';
return ($this->charset === '')
? $func($this->username, $this->password, $this->DSN)
: $func($this->username, $this->password, $this->DSN, $this->charset);
}
/**
* Keep or establish the connection if no queries have been sent for
* a length of time exceeding the server's idle timeout.
*
* @return void
*/
public function reconnect()
{
}
/**
* Close the database connection.
*
* @return void
*/
protected function _close()
{
if (is_resource($this->cursorId)) {
oci_free_statement($this->cursorId);
}
if (is_resource($this->stmtId)) {
oci_free_statement($this->stmtId);
}
oci_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 || ($versionString = oci_server_version($this->connID)) === false) {
return '';
}
if (preg_match('#Release\s(\d+(?:\.\d+)+)#', $versionString, $match)) {
return $this->dataCache['version'] = $match[1];
}
return '';
}
/**
* Executes the query against the database.
*
* @return false|resource
*/
protected function execute(string $sql)
{
try {
if ($this->resetStmtId === true) {
$this->stmtId = oci_parse($this->connID, $sql);
}
oci_set_prefetch($this->stmtId, 1000);
$result = oci_execute($this->stmtId, $this->commitMode) ? $this->stmtId : false;
$insertTableName = $this->parseInsertTableName($sql);
if ($result && $insertTableName !== '') {
$this->lastInsertedTableName = $insertTableName;
}
return $result;
} catch (ErrorException $e) {
log_message('error', (string) $e);
if ($this->DBDebug) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
}
}
return false;
}
/**
* Get the table name for the insert statement from sql.
*/
public function parseInsertTableName(string $sql): string
{
$commentStrippedSql = preg_replace(['/\/\*(.|\n)*?\*\//m', '/--.+/'], '', $sql);
$isInsertQuery = str_starts_with(strtoupper(ltrim($commentStrippedSql)), 'INSERT');
if (! $isInsertQuery) {
return '';
}
preg_match('/(?is)\b(?:into)\s+("?\w+"?)/', $commentStrippedSql, $match);
$tableName = $match[1] ?? '';
return str_starts_with($tableName, '"') ? trim($tableName, '"') : strtoupper($tableName);
}
/**
* Returns the total number of rows affected by this query.
*/
public function affectedRows(): int
{
return oci_num_rows($this->stmtId);
}
/**
* Generates the SQL for listing tables in a platform-dependent manner.
*
* @param string|null $tableName If $tableName is provided will return only this table if exists.
*/
protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
{
$sql = 'SELECT "TABLE_NAME" FROM "USER_TABLES"';
if ($tableName !== null) {
return $sql . ' WHERE "TABLE_NAME" LIKE ' . $this->escape($tableName);
}
if ($prefixLimit !== false && $this->DBPrefix !== '') {
return $sql . ' WHERE "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
{
if (str_contains($table, '.')) {
sscanf($table, '%[^.].%s', $owner, $table);
} else {
$owner = $this->username;
}
return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS
WHERE UPPER(OWNER) = ' . $this->escape(strtoupper($owner)) . '
AND UPPER(TABLE_NAME) = ' . $this->escape(strtoupper($this->DBPrefix . $table));
}
/**
* Returns an array of objects with field data
*
* @return list<stdClass>
*
* @throws DatabaseException
*/
protected function _fieldData(string $table): array
{
if (str_contains($table, '.')) {
sscanf($table, '%[^.].%s', $owner, $table);
} else {
$owner = $this->username;
}
$sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHAR_LENGTH, DATA_PRECISION, DATA_LENGTH, DATA_DEFAULT, NULLABLE
FROM ALL_TAB_COLUMNS
WHERE UPPER(OWNER) = ' . $this->escape(strtoupper($owner)) . '
AND UPPER(TABLE_NAME) = ' . $this->escape(strtoupper($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;
$length = $query[$i]->CHAR_LENGTH > 0 ? $query[$i]->CHAR_LENGTH : $query[$i]->DATA_PRECISION;
$length ??= $query[$i]->DATA_LENGTH;
$retval[$i]->max_length = $length;
$retval[$i]->nullable = $query[$i]->NULLABLE === 'Y';
$retval[$i]->default = $query[$i]->DATA_DEFAULT;
}
return $retval;
}
/**
* Returns an array of objects with index data
*
* @return array<string, stdClass>
*
* @throws DatabaseException
*/
protected function _indexData(string $table): array
{
if (str_contains($table, '.')) {
sscanf($table, '%[^.].%s', $owner, $table);
} else {
$owner = $this->username;
}
$sql = 'SELECT AIC.INDEX_NAME, UC.CONSTRAINT_TYPE, AIC.COLUMN_NAME '
. ' FROM ALL_IND_COLUMNS AIC '
. ' LEFT JOIN USER_CONSTRAINTS UC ON AIC.INDEX_NAME = UC.CONSTRAINT_NAME AND AIC.TABLE_NAME = UC.TABLE_NAME '
. 'WHERE AIC.TABLE_NAME = ' . $this->escape(strtolower($table)) . ' '
. 'AND AIC.TABLE_OWNER = ' . $this->escape(strtoupper($owner)) . ' '
. ' ORDER BY UC.CONSTRAINT_TYPE, AIC.COLUMN_POSITION';
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetIndexData'));
}
$query = $query->getResultObject();
$retVal = [];
$constraintTypes = [
'P' => 'PRIMARY',
'U' => 'UNIQUE',
];
foreach ($query as $row) {
if (isset($retVal[$row->INDEX_NAME])) {
$retVal[$row->INDEX_NAME]->fields[] = $row->COLUMN_NAME;
continue;
}
$retVal[$row->INDEX_NAME] = new stdClass();
$retVal[$row->INDEX_NAME]->name = $row->INDEX_NAME;
$retVal[$row->INDEX_NAME]->fields = [$row->COLUMN_NAME];
$retVal[$row->INDEX_NAME]->type = $constraintTypes[$row->CONSTRAINT_TYPE] ?? 'INDEX';
}
return $retVal;
}
/**
* Returns an array of objects with Foreign key data
*
* @return array<string, stdClass>
*
* @throws DatabaseException
*/
protected function _foreignKeyData(string $table): array
{
$sql = 'SELECT
acc.constraint_name,
acc.table_name,
acc.column_name,
ccu.table_name foreign_table_name,
accu.column_name foreign_column_name,
ac.delete_rule
FROM all_cons_columns acc
JOIN all_constraints ac ON acc.owner = ac.owner
AND acc.constraint_name = ac.constraint_name
JOIN all_constraints ccu ON ac.r_owner = ccu.owner
AND ac.r_constraint_name = ccu.constraint_name
JOIN all_cons_columns accu ON accu.constraint_name = ccu.constraint_name
AND accu.position = acc.position
AND accu.table_name = ccu.table_name
WHERE ac.constraint_type = ' . $this->escape('R') . '
AND acc.table_name = ' . $this->escape($table);
$query = $this->query($sql);
if ($query === false) {
throw new DatabaseException(lang('Database.failGetForeignKeyData'));
}
$query = $query->getResultObject();
$indexes = [];
foreach ($query as $row) {
$indexes[$row->CONSTRAINT_NAME]['constraint_name'] = $row->CONSTRAINT_NAME;
$indexes[$row->CONSTRAINT_NAME]['table_name'] = $row->TABLE_NAME;
$indexes[$row->CONSTRAINT_NAME]['column_name'][] = $row->COLUMN_NAME;
$indexes[$row->CONSTRAINT_NAME]['foreign_table_name'] = $row->FOREIGN_TABLE_NAME;
$indexes[$row->CONSTRAINT_NAME]['foreign_column_name'][] = $row->FOREIGN_COLUMN_NAME;
$indexes[$row->CONSTRAINT_NAME]['on_delete'] = $row->DELETE_RULE;
$indexes[$row->CONSTRAINT_NAME]['on_update'] = null;
$indexes[$row->CONSTRAINT_NAME]['match'] = null;
}
return $this->foreignKeyDataToObjects($indexes);
}
/**
* Returns platform-specific SQL to disable foreign key checks.
*
* @return string
*/
protected function _disableForeignKeyChecks()
{
return <<<'SQL'
BEGIN
FOR c IN
(SELECT c.owner, c.table_name, c.constraint_name
FROM user_constraints c, user_tables t
WHERE c.table_name = t.table_name
AND c.status = 'ENABLED'
AND c.constraint_type = 'R'
AND t.iot_type IS NULL
ORDER BY c.constraint_type DESC)
LOOP
dbms_utility.exec_ddl_statement('alter table "' || c.owner || '"."' || c.table_name || '" disable constraint "' || c.constraint_name || '"');
END LOOP;
END;
SQL;
}
/**
* Returns platform-specific SQL to enable foreign key checks.
*
* @return string
*/
protected function _enableForeignKeyChecks()
{
return <<<'SQL'
BEGIN
FOR c IN
(SELECT c.owner, c.table_name, c.constraint_name
FROM user_constraints c, user_tables t
WHERE c.table_name = t.table_name
AND c.status = 'DISABLED'
AND c.constraint_type = 'R'
AND t.iot_type IS NULL
ORDER BY c.constraint_type DESC)
LOOP
dbms_utility.exec_ddl_statement('alter table "' || c.owner || '"."' || c.table_name || '" enable constraint "' || c.constraint_name || '"');
END LOOP;
END;
SQL;
}
/**
* Get cursor. Returns a cursor from the database
*
* @return resource
*/
public function getCursor()
{
return $this->cursorId = oci_new_cursor($this->connID);
}
/**
* Executes a stored procedure
*
* @param string $procedureName procedure name to execute
* @param array $params params array keys
* KEY OPTIONAL NOTES
* name no the name of the parameter should be in :<param_name> format
* value no the value of the parameter. If this is an OUT or IN OUT parameter,
* this should be a reference to a variable
* type yes the type of the parameter
* length yes the max size of the parameter
*
* @return bool|Query|Result
*/
public function storedProcedure(string $procedureName, array $params)
{
if ($procedureName === '') {
throw new DatabaseException(lang('Database.invalidArgument', [$procedureName]));
}
// Build the query string
$sql = sprintf(
'BEGIN %s (' . substr(str_repeat(',%s', count($params)), 1) . '); END;',
$procedureName,
...array_map(static fn ($row) => $row['name'], $params)
);
$this->resetStmtId = false;
$this->stmtId = oci_parse($this->connID, $sql);
$this->bindParams($params);
$result = $this->query($sql);
$this->resetStmtId = true;
return $result;
}
/**
* Bind parameters
*
* @param array $params
*
* @return void
*/
protected function bindParams($params)
{
if (! is_array($params) || ! is_resource($this->stmtId)) {
return;
}
foreach ($params as $param) {
oci_bind_by_name(
$this->stmtId,
$param['name'],
$param['value'],
$param['length'] ?? -1,
$param['type'] ?? SQLT_CHR
);
}
}
/**
* Returns the last error code and message.
*
* Must return an array with keys 'code' and 'message':
*
* return ['code' => null, 'message' => null);
*/
public function error(): array
{
// oci_error() returns an array that already contains
// 'code' and 'message' keys, but it can return false
// if there was no error ....
$error = oci_error();
$resources = [$this->cursorId, $this->stmtId, $this->connID];
foreach ($resources as $resource) {
if (is_resource($resource)) {
$error = oci_error($resource);
break;
}
}
return is_array($error)
? $error
: [
'code' => '',
'message' => '',
];
}
public function insertID(): int
{
if (empty($this->lastInsertedTableName)) {
return 0;
}
$indexs = $this->getIndexData($this->lastInsertedTableName);
$fieldDatas = $this->getFieldData($this->lastInsertedTableName);
if ($indexs === [] || $fieldDatas === []) {
return 0;
}
$columnTypeList = array_column($fieldDatas, 'type', 'name');
$primaryColumnName = '';
foreach ($indexs as $index) {
if ($index->type !== 'PRIMARY' || count($index->fields) !== 1) {
continue;
}
$primaryColumnName = $this->protectIdentifiers($index->fields[0], false, false);
$primaryColumnType = $columnTypeList[$primaryColumnName];
if ($primaryColumnType !== 'NUMBER') {
$primaryColumnName = '';
}
}
if ($primaryColumnName === '') {
return 0;
}
$query = $this->query('SELECT DATA_DEFAULT FROM USER_TAB_COLUMNS WHERE TABLE_NAME = ? AND COLUMN_NAME = ?', [$this->lastInsertedTableName, $primaryColumnName])->getRow();
$lastInsertValue = str_replace('nextval', 'currval', $query->DATA_DEFAULT ?? '0');
$query = $this->query(sprintf('SELECT %s SEQ FROM DUAL', $lastInsertValue))->getRow();
return (int) ($query->SEQ ?? 0);
}
/**
* Build a DSN from the provided parameters
*
* @return void
*/
protected function buildDSN()
{
if ($this->DSN !== '') {
$this->DSN = '';
}
// Legacy support for TNS in the hostname configuration field
$this->hostname = str_replace(["\n", "\r", "\t", ' '], '', $this->hostname);
if (preg_match($this->validDSNs['tns'], $this->hostname)) {
$this->DSN = $this->hostname;
return;
}
$isEasyConnectableHostName = $this->hostname !== '' && ! str_contains($this->hostname, '/') && ! str_contains($this->hostname, ':');
$easyConnectablePort = ($this->port !== '') && ctype_digit((string) $this->port) ? ':' . $this->port : '';
$easyConnectableDatabase = $this->database !== '' ? '/' . ltrim($this->database, '/') : '';
if ($isEasyConnectableHostName && ($easyConnectablePort !== '' || $easyConnectableDatabase !== '')) {
/* If the hostname field isn't empty, doesn't contain
* ':' and/or '/' and if port and/or database aren't
* empty, then the hostname field is most likely indeed
* just a hostname. Therefore we'll try and build an
* Easy Connect string from these 3 settings, assuming
* that the database field is a service name.
*/
$this->DSN = $this->hostname . $easyConnectablePort . $easyConnectableDatabase;
if (preg_match($this->validDSNs['ec'], $this->DSN)) {
return;
}
}
/* At this point, we can only try and validate the hostname and
* database fields separately as DSNs.
*/
if (preg_match($this->validDSNs['ec'], $this->hostname) || preg_match($this->validDSNs['in'], $this->hostname)) {
$this->DSN = $this->hostname;
return;
}
$this->database = str_replace(["\n", "\r", "\t", ' '], '', $this->database);
foreach ($this->validDSNs as $regexp) {
if (preg_match($regexp, $this->database)) {
return;
}
}
/* Well - OK, an empty string should work as well.
* PHP will try to use environment variables to
* determine which Oracle instance to connect to.
*/
$this->DSN = '';
}
/**
* Begin Transaction
*/
protected function _transBegin(): bool
{
$this->commitMode = OCI_NO_AUTO_COMMIT;
return true;
}
/**
* Commit Transaction
*/
protected function _transCommit(): bool
{
$this->commitMode = OCI_COMMIT_ON_SUCCESS;
return oci_commit($this->connID);
}
/**
* Rollback Transaction
*/
protected function _transRollback(): bool
{
$this->commitMode = OCI_COMMIT_ON_SUCCESS;
return oci_rollback($this->connID);
}
/**
* Returns the name of the current database being used.
*/
public function getDatabase(): string
{
if (! empty($this->database)) {
return $this->database;
}
return $this->query('SELECT DEFAULT_TABLESPACE FROM USER_USERS')->getRow()->DEFAULT_TABLESPACE ?? '';
}
/**
* Get the prefix of the function to access the DB.
*/
protected function getDriverFunctionPrefix(): string
{
return 'oci_';
}
}
+314
View File
@@ -0,0 +1,314 @@
<?php
declare(strict_types=1);
/**
* 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\OCI8;
use CodeIgniter\Database\Forge as BaseForge;
/**
* Forge for OCI8
*/
class Forge extends BaseForge
{
/**
* DROP INDEX statement
*
* @var string
*/
protected $dropIndexStr = 'DROP INDEX %s';
/**
* CREATE DATABASE statement
*
* @var false
*/
protected $createDatabaseStr = false;
/**
* CREATE TABLE IF statement
*
* @var false
*
* @deprecated This is no longer used.
*/
protected $createTableIfStr = false;
/**
* DROP TABLE IF EXISTS statement
*
* @var false
*/
protected $dropTableIfStr = false;
/**
* DROP DATABASE statement
*
* @var false
*/
protected $dropDatabaseStr = false;
/**
* UNSIGNED support
*
* @var array|bool
*/
protected $unsigned = false;
/**
* NULL value representation in CREATE/ALTER TABLE statements
*
* @var string
*/
protected $null = 'NULL';
/**
* RENAME TABLE statement
*
* @var string
*/
protected $renameTableStr = 'ALTER TABLE %s RENAME TO %s';
/**
* DROP CONSTRAINT statement
*
* @var string
*/
protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s';
/**
* Foreign Key Allowed Actions
*
* @var array
*/
protected $fkAllowActions = ['CASCADE', 'SET NULL', 'NO ACTION'];
/**
* ALTER TABLE
*
* @param string $alterType ALTER type
* @param string $table Table name
* @param array|string $processedFields Processed column definitions
* or column names to DROP
*
* @return list<string>|string SQL string
* @phpstan-return ($alterType is 'DROP' ? string : list<string>)
*/
protected function _alterTable(string $alterType, string $table, $processedFields)
{
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
if ($alterType === 'DROP') {
$columnNamesToDrop = $processedFields;
$fields = array_map(
fn ($field) => $this->db->escapeIdentifiers(trim($field)),
is_string($columnNamesToDrop) ? explode(',', $columnNamesToDrop) : $columnNamesToDrop
);
return $sql . ' DROP (' . implode(',', $fields) . ') CASCADE CONSTRAINT INVALIDATE';
}
if ($alterType === 'CHANGE') {
$alterType = 'MODIFY';
}
$nullableMap = array_column($this->db->getFieldData($table), 'nullable', 'name');
$sqls = [];
for ($i = 0, $c = count($processedFields); $i < $c; $i++) {
if ($alterType === 'MODIFY') {
// If a null constraint is added to a column with a null constraint,
// ORA-01451 will occur,
// so add null constraint is used only when it is different from the current null constraint.
// If a not null constraint is added to a column with a not null constraint,
// ORA-01442 will occur.
$wantToAddNull = ! str_contains($processedFields[$i]['null'], ' NOT');
$currentNullable = $nullableMap[$processedFields[$i]['name']];
if ($wantToAddNull === true && $currentNullable === true) {
$processedFields[$i]['null'] = '';
} elseif ($processedFields[$i]['null'] === '' && $currentNullable === false) {
// Nullable by default
$processedFields[$i]['null'] = ' NULL';
} elseif ($wantToAddNull === false && $currentNullable === false) {
$processedFields[$i]['null'] = '';
}
}
if ($processedFields[$i]['_literal'] !== false) {
$processedFields[$i] = "\n\t" . $processedFields[$i]['_literal'];
} else {
$processedFields[$i]['_literal'] = "\n\t" . $this->_processColumn($processedFields[$i]);
if (! empty($processedFields[$i]['comment'])) {
$sqls[] = 'COMMENT ON COLUMN '
. $this->db->escapeIdentifiers($table) . '.' . $this->db->escapeIdentifiers($processedFields[$i]['name'])
. ' IS ' . $processedFields[$i]['comment'];
}
if ($alterType === 'MODIFY' && ! empty($processedFields[$i]['new_name'])) {
$sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($processedFields[$i]['name'])
. ' TO ' . $this->db->escapeIdentifiers($processedFields[$i]['new_name']);
}
$processedFields[$i] = "\n\t" . $processedFields[$i]['_literal'];
}
}
$sql .= ' ' . $alterType . ' ';
$sql .= count($processedFields) === 1
? $processedFields[0]
: '(' . implode(',', $processedFields) . ')';
// RENAME COLUMN must be executed after MODIFY
array_unshift($sqls, $sql);
return $sqls;
}
/**
* Field attribute AUTO_INCREMENT
*
* @return void
*/
protected function _attributeAutoIncrement(array &$attributes, array &$field)
{
if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true
&& stripos($field['type'], 'NUMBER') !== false
&& version_compare($this->db->getVersion(), '12.1', '>=')
) {
$field['auto_increment'] = ' GENERATED BY DEFAULT ON NULL AS IDENTITY';
}
}
/**
* Process column
*/
protected function _processColumn(array $processedField): string
{
$constraint = '';
// @todo: can't cover multi pattern when set type.
if ($processedField['type'] === 'VARCHAR2' && str_starts_with($processedField['length'], "('")) {
$constraint = ' CHECK(' . $this->db->escapeIdentifiers($processedField['name'])
. ' IN ' . $processedField['length'] . ')';
$processedField['length'] = '(' . max(array_map(mb_strlen(...), explode("','", mb_substr($processedField['length'], 2, -2)))) . ')' . $constraint;
} elseif (isset($this->primaryKeys['fields']) && count($this->primaryKeys['fields']) === 1 && $processedField['name'] === $this->primaryKeys['fields'][0]) {
$processedField['unique'] = '';
}
return $this->db->escapeIdentifiers($processedField['name'])
. ' ' . $processedField['type'] . $processedField['length']
. $processedField['unsigned']
. $processedField['default']
. $processedField['auto_increment']
. $processedField['null']
. $processedField['unique'];
}
/**
* Performs a data type mapping between different databases.
*
* @return void
*/
protected function _attributeType(array &$attributes)
{
// Reset field lengths for data types that don't support it
// Usually overridden by drivers
switch (strtoupper($attributes['TYPE'])) {
case 'TINYINT':
$attributes['CONSTRAINT'] ??= 3;
// no break
case 'SMALLINT':
$attributes['CONSTRAINT'] ??= 5;
// no break
case 'MEDIUMINT':
$attributes['CONSTRAINT'] ??= 7;
// no break
case 'INT':
case 'INTEGER':
$attributes['CONSTRAINT'] ??= 11;
// no break
case 'BIGINT':
$attributes['CONSTRAINT'] ??= 19;
// no break
case 'NUMERIC':
$attributes['TYPE'] = 'NUMBER';
return;
case 'BOOLEAN':
$attributes['TYPE'] = 'NUMBER';
$attributes['CONSTRAINT'] = 1;
$attributes['UNSIGNED'] = true;
return;
case 'DOUBLE':
$attributes['TYPE'] = 'FLOAT';
$attributes['CONSTRAINT'] ??= 126;
return;
case 'DATETIME':
case 'TIME':
$attributes['TYPE'] = 'DATE';
return;
case 'SET':
case 'ENUM':
case 'VARCHAR':
$attributes['CONSTRAINT'] ??= 255;
// no break
case 'TEXT':
case 'MEDIUMTEXT':
$attributes['CONSTRAINT'] ??= 4000;
$attributes['TYPE'] = 'VARCHAR2';
}
}
/**
* Generates a platform-specific DROP TABLE string
*
* @return bool|string
*/
protected function _dropTable(string $table, bool $ifExists, bool $cascade)
{
$sql = parent::_dropTable($table, $ifExists, $cascade);
if ($sql !== true && $cascade === true) {
$sql .= ' CASCADE CONSTRAINTS PURGE';
} elseif ($sql !== true) {
$sql .= ' PURGE';
}
return $sql;
}
/**
* Constructs sql to check if key is a constraint.
*/
protected function _dropKeyAsConstraint(string $table, string $constraintName): string
{
return "SELECT constraint_name FROM all_constraints WHERE table_name = '"
. trim($table, '"') . "' AND index_name = '"
. trim($constraintName, '"') . "'";
}
}
+120
View File
@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
/**
* 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\OCI8;
use BadMethodCallException;
use CodeIgniter\Database\BasePreparedQuery;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Prepared query for OCI8
*
* @extends BasePreparedQuery<resource, resource, resource>
*/
class PreparedQuery extends BasePreparedQuery
{
/**
* A reference to the db connection to use.
*
* @var Connection
*/
protected $db;
/**
* Latest inserted table name.
*/
private ?string $lastInsertTableName = null;
/**
* 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 OCI8 driver.
*/
public function _prepare(string $sql, array $options = []): PreparedQuery
{
if (! $this->statement = oci_parse($this->db->connID, $this->parameterize($sql))) {
$error = oci_error($this->db->connID);
$this->errorCode = $error['code'] ?? 0;
$this->errorString = $error['message'] ?? '';
if ($this->db->DBDebug) {
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
}
}
$this->lastInsertTableName = $this->db->parseInsertTableName($sql);
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 (array_keys($data) as $key) {
oci_bind_by_name($this->statement, ':' . $key, $data[$key]);
}
$result = oci_execute($this->statement, $this->db->commitMode);
if ($result && $this->lastInsertTableName !== '') {
$this->db->lastInsertedTableName = $this->lastInsertTableName;
}
return $result;
}
/**
* Returns the statement resource for the prepared query or false when preparing failed.
*
* @return resource|null
*/
public function _getResult()
{
return $this->statement;
}
/**
* Deallocate prepared statements.
*/
protected function _close(): bool
{
return oci_free_statement($this->statement);
}
/**
* Replaces the ? placeholders with :0, :1, 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 ($matches) use (&$count) {
return ':' . ($count++);
}, $sql);
}
}
+119
View File
@@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
/**
* 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\OCI8;
use CodeIgniter\Database\BaseResult;
use CodeIgniter\Entity\Entity;
use stdClass;
/**
* Result for OCI8
*
* @extends BaseResult<resource, resource>
*/
class Result extends BaseResult
{
/**
* Gets the number of fields in the result set.
*/
public function getFieldCount(): int
{
return oci_num_fields($this->resultID);
}
/**
* Generates an array of column names in the result set.
*/
public function getFieldNames(): array
{
return array_map(fn ($fieldIndex) => oci_field_name($this->resultID, $fieldIndex), range(1, $this->getFieldCount()));
}
/**
* Generates an array of objects representing field meta-data.
*/
public function getFieldData(): array
{
return array_map(fn ($fieldIndex) => (object) [
'name' => oci_field_name($this->resultID, $fieldIndex),
'type' => oci_field_type($this->resultID, $fieldIndex),
'max_length' => oci_field_size($this->resultID, $fieldIndex),
], range(1, $this->getFieldCount()));
}
/**
* Frees the current result.
*
* @return void
*/
public function freeResult()
{
if (is_resource($this->resultID)) {
oci_free_statement($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 false
*/
public function dataSeek(int $n = 0)
{
// We can't support data seek by oci
return false;
}
/**
* Returns the result set as an array.
*
* Overridden by driver classes.
*
* @return array|false
*/
protected function fetchAssoc()
{
return oci_fetch_assoc($this->resultID);
}
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @return Entity|false|object|stdClass
*/
protected function fetchObject(string $className = 'stdClass')
{
$row = oci_fetch_object($this->resultID);
if ($className === 'stdClass' || ! $row) {
return $row;
}
if (is_subclass_of($className, Entity::class)) {
return (new $className())->injectRawData((array) $row);
}
$instance = new $className();
foreach (get_object_vars($row) as $key => $value) {
$instance->{$key} = $value;
}
return $instance;
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* 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\OCI8;
use CodeIgniter\Database\BaseUtils;
use CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Utils for OCI8
*/
class Utils extends BaseUtils
{
/**
* List databases statement
*
* @var string
*/
protected $listDatabases = 'SELECT TABLESPACE_NAME FROM USER_TABLESPACES';
/**
* Platform dependent version of the backup function.
*
* @return never
*/
public function _backup(?array $prefs = null)
{
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
}
+634
View File
@@ -0,0 +1,634 @@
<?php
declare(strict_types=1);
/**
* 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;
use CodeIgniter\Database\RawSql;
use InvalidArgumentException;
/**
* 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.
*
* @return mixed
*
* @throws DatabaseException
*/
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}"]);
if (! $this->testMode) {
$this->resetWrite();
return $this->db->query($sql, $this->binds, false);
}
return true;
}
/**
* Decrements a numeric column by the specified value.
*
* @return mixed
*
* @throws DatabaseException
*/
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}"]);
if (! $this->testMode) {
$this->resetWrite();
return $this->db->query($sql, $this->binds, false);
}
return true;
}
/**
* 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
*
* @return mixed
*
* @throws DatabaseException
*/
public function replace(?array $set = null)
{
if ($set !== null) {
$this->set($set);
}
if ($this->QBSet === []) {
if ($this->db->DBDebug) {
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): void {
$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();
$this->binds = [];
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
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$sql = 'INSERT INTO ' . $table . '(' . implode(', ', $keys) . ")\n{:_table_:}\n";
$sql .= $this->compileIgnore('insert');
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = 'VALUES ' . implode(', ', $this->formatValues($values));
}
return str_replace('{:_table_:}', $data, $sql);
}
/**
* Compiles a delete string and runs the query
*
* @param mixed $where
*
* @return mixed
*
* @throws DatabaseException
*/
public function delete($where = '', ?int $limit = null, bool $resetData = true)
{
if ($limit !== null && $limit !== 0 || ! 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 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
*
* @param RawSql|string $cond
*
* @return BaseBuilder
*/
public function join(string $table, $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);
}
/**
* Generates a platform-specific batch update string from the supplied data
*
* @used-by batchExecute()
*
* @param string $table Protected table name
* @param list<string> $keys QBKeys
* @param list<list<int|string>> $values QBSet
*/
protected function _updateBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$constraints = $this->QBOptions['constraints'] ?? [];
if ($constraints === []) {
if ($this->db->DBDebug) {
throw new DatabaseException('You must specify a constraint to match on for batch updates.'); // @codeCoverageIgnore
}
return ''; // @codeCoverageIgnore
}
$updateFields = $this->QBOptions['updateFields'] ??
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
[];
$alias = $this->QBOptions['alias'] ?? '_u';
$sql = 'UPDATE ' . $this->compileIgnore('update') . $table . "\n";
$sql .= "SET\n";
$that = $this;
$sql .= implode(
",\n",
array_map(
static fn ($key, $value) => $key . ($value instanceof RawSql ?
' = ' . $value :
' = ' . $that->cast($alias . '.' . $value, $that->getFieldType($table, $key))),
array_keys($updateFields),
$updateFields
)
) . "\n";
$sql .= "FROM (\n{:_table_:}";
$sql .= ') ' . $alias . "\n";
$sql .= 'WHERE ' . implode(
' AND ',
array_map(
static function ($key, $value) use ($table, $alias, $that) {
if ($value instanceof RawSql && is_string($key)) {
return $table . '.' . $key . ' = ' . $value;
}
if ($value instanceof RawSql) {
return $value;
}
return $table . '.' . $value . ' = '
. $that->cast($alias . '.' . $value, $that->getFieldType($table, $value));
},
array_keys($constraints),
$constraints
)
);
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = implode(
" UNION ALL\n",
array_map(
static fn ($value) => 'SELECT ' . implode(', ', array_map(
static fn ($key, $index) => $index . ' ' . $key,
$keys,
$value
)),
$values
)
) . "\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
/**
* Returns cast expression.
*
* @TODO move this to BaseBuilder in 4.5.0
*
* @param float|int|string $expression
*/
private function cast($expression, ?string $type): string
{
return ($type === null) ? $expression : 'CAST(' . $expression . ' AS ' . strtoupper($type) . ')';
}
/**
* Returns the filed type from database meta data.
*
* @param string $table Protected table name.
* @param string $fieldName Field name. May be protected.
*/
private function getFieldType(string $table, string $fieldName): ?string
{
$fieldName = trim($fieldName, $this->db->escapeChar);
if (! isset($this->QBOptions['fieldTypes'][$table])) {
$this->QBOptions['fieldTypes'][$table] = [];
foreach ($this->db->getFieldData($table) as $field) {
$type = $field->type;
// If `character` (or `char`) lacks a specifier, it is equivalent
// to `character(1)`.
// See https://www.postgresql.org/docs/current/datatype-character.html
if ($field->type === 'character') {
$type = $field->type . '(' . $field->max_length . ')';
}
$this->QBOptions['fieldTypes'][$table][$field->name] = $type;
}
}
return $this->QBOptions['fieldTypes'][$table][$fieldName] ?? null;
}
/**
* Generates a platform-specific upsertBatch string from the supplied data
*
* @throws DatabaseException
*/
protected function _upsertBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$fieldNames = array_map(static fn ($columnName) => trim($columnName, '"'), $keys);
$constraints = $this->QBOptions['constraints'] ?? [];
if (empty($constraints)) {
$allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames) {
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
return ($index->type === 'UNIQUE' || $index->type === 'PRIMARY') && $hasAllFields;
});
foreach (array_map(static fn ($index) => $index->fields, $allIndexes) as $index) {
$constraints[] = current($index);
// only one index can be used?
break;
}
$constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? [];
}
if (empty($constraints)) {
if ($this->db->DBDebug) {
throw new DatabaseException('No constraint found for upsert.');
}
return ''; // @codeCoverageIgnore
}
// in value set - replace null with DEFAULT where constraint is presumed not null
// autoincrement identity field must use DEFAULT and not NULL
// this could be removed in favour of leaving to developer but does make things easier and function like other DBMS
foreach ($constraints as $constraint) {
$key = array_search(trim((string) $constraint, '"'), $fieldNames, true);
if ($key !== false) {
foreach ($values as $arrayKey => $value) {
if (strtoupper((string) $value[$key]) === 'NULL') {
$values[$arrayKey][$key] = 'DEFAULT';
}
}
}
}
$alias = $this->QBOptions['alias'] ?? '"excluded"';
if (strtolower($alias) !== '"excluded"') {
throw new InvalidArgumentException('Postgres alias is always named "excluded". A custom alias cannot be used.');
}
$updateFields = $this->QBOptions['updateFields'] ?? $this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ?? [];
$sql = 'INSERT INTO ' . $table . ' (';
$sql .= implode(', ', $keys);
$sql .= ")\n";
$sql .= '{:_table_:}';
$sql .= 'ON CONFLICT(' . implode(',', $constraints) . ")\n";
$sql .= "DO UPDATE SET\n";
$sql .= implode(
",\n",
array_map(
static fn ($key, $value) => $key . ($value instanceof RawSql ?
" = {$value}" :
" = {$alias}.{$value}"),
array_keys($updateFields),
$updateFields
)
);
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
/**
* Generates a platform-specific batch update string from the supplied data
*/
protected function _deleteBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$constraints = $this->QBOptions['constraints'] ?? [];
if ($constraints === []) {
if ($this->db->DBDebug) {
throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore
}
return ''; // @codeCoverageIgnore
}
$alias = $this->QBOptions['alias'] ?? '_u';
$sql = 'DELETE FROM ' . $table . "\n";
$sql .= "USING (\n{:_table_:}";
$sql .= ') ' . $alias . "\n";
$that = $this;
$sql .= 'WHERE ' . implode(
' AND ',
array_map(
static function ($key, $value) use ($table, $alias, $that) {
if ($value instanceof RawSql) {
return $value;
}
if (is_string($key)) {
return $table . '.' . $key . ' = '
. $that->cast(
$alias . '.' . $value,
$that->getFieldType($table, $key)
);
}
return $table . '.' . $value . ' = ' . $alias . '.' . $value;
},
array_keys($constraints),
$constraints
)
);
// convert binds in where
foreach ($this->QBWhere as $key => $where) {
foreach ($this->binds as $field => $bind) {
$this->QBWhere[$key]['condition'] = str_replace(':' . $field . ':', $bind[0], $where['condition']);
}
}
$sql .= ' ' . str_replace(
'WHERE ',
'AND ',
$this->compileWhereHaving('QBWhere')
);
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = implode(
" UNION ALL\n",
array_map(
static fn ($value) => 'SELECT ' . implode(', ', array_map(
static fn ($key, $index) => $index . ' ' . $key,
$keys,
$value
)),
$values
)
) . "\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
}
+589
View File
@@ -0,0 +1,589 @@
<?php
declare(strict_types=1);
/**
* 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 CodeIgniter\Database\RawSql;
use ErrorException;
use PgSql\Connection as PgSqlConnection;
use PgSql\Result as PgSqlResult;
use stdClass;
use Stringable;
/**
* Connection for Postgre
*
* @extends BaseConnection<PgSqlConnection, PgSqlResult>
*/
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 = '"';
protected $connect_timeout;
protected $options;
protected $sslmode;
protected $service;
/**
* Connect to the database.
*
* @return false|PgSqlConnection
*/
public function connect(bool $persistent = false)
{
if (empty($this->DSN)) {
$this->buildDSN();
}
// Convert DSN string
// @TODO This format is for PDO_PGSQL.
// https://www.php.net/manual/en/ref.pdo-pgsql.connection.php
// Should deprecate?
if (mb_strpos($this->DSN, 'pgsql:') === 0) {
$this->convertDSN();
}
$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
) {
$error = pg_last_error($this->connID);
throw new DatabaseException($error);
}
if (! empty($this->schema)) {
$this->simpleQuery("SET search_path TO {$this->schema},public");
}
if ($this->setClientEncoding($this->charset) === false) {
$error = pg_last_error($this->connID);
throw new DatabaseException($error);
}
}
return $this->connID;
}
/**
* Converts the DSN with semicolon syntax.
*
* @return void
*/
private function convertDSN()
{
// Strip pgsql
$this->DSN = mb_substr($this->DSN, 6);
// Convert semicolons to spaces in DSN format like:
// pgsql:host=localhost;port=5432;dbname=database_name
// https://www.php.net/manual/en/function.pg-connect.php
$allowedParams = ['host', 'port', 'dbname', 'user', 'password', 'connect_timeout', 'options', 'sslmode', 'service'];
$parameters = explode(';', $this->DSN);
$output = '';
$previousParameter = '';
foreach ($parameters as $parameter) {
[$key, $value] = explode('=', $parameter, 2);
if (in_array($key, $allowedParams, true)) {
if ($previousParameter !== '') {
if (array_search($key, $allowedParams, true) < array_search($previousParameter, $allowedParams, true)) {
$output .= ';';
} else {
$output .= ' ';
}
}
$output .= $parameter;
$previousParameter = $key;
} else {
$output .= ';' . $parameter;
}
}
$this->DSN = $output;
}
/**
* Keep or establish the connection if no queries have been sent for
* a length of time exceeding the server's idle timeout.
*
* @return void
*/
public function reconnect()
{
if (pg_ping($this->connID) === false) {
$this->connID = false;
}
}
/**
* Close the database connection.
*
* @return void
*/
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) {
$this->initialize();
}
$pgVersion = pg_version($this->connID);
$this->dataCache['version'] = isset($pgVersion['server']) ?
(preg_match('/^(\d+\.\d+)/', $pgVersion['server'], $matches) ? $matches[1] : '') :
'';
return $this->dataCache['version'];
}
/**
* Executes the query against the database.
*
* @return false|PgSqlResult
*/
protected function execute(string $sql)
{
try {
return pg_query($this->connID, $sql);
} catch (ErrorException $e) {
log_message('error', (string) $e);
if ($this->DBDebug) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $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 array|bool|float|int|object|string|null $str
*
* @return array|float|int|string
* @phpstan-return ($str is array ? array : float|int|string)
*/
public function escape($str)
{
if (! $this->connID) {
$this->initialize();
}
if ($str instanceof Stringable) {
if ($str instanceof RawSql) {
return $str->__toString();
}
$str = (string) $str;
}
if (is_string($str)) {
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.
*
* @param string|null $tableName If $tableName is provided will return only this table if exists.
*/
protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
{
$sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \'' . $this->schema . "'";
if ($tableName !== null) {
return $sql . ' AND "table_name" LIKE ' . $this->escape($tableName);
}
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
*
* @return list<stdClass>
*
* @throws DatabaseException
*/
protected function _fieldData(string $table): array
{
$sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default", "is_nullable"
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]->max_length = $query[$i]->character_maximum_length > 0 ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision;
$retVal[$i]->nullable = $query[$i]->is_nullable === 'YES';
$retVal[$i]->default = $query[$i]->column_default;
}
return $retVal;
}
/**
* Returns an array of objects with index data
*
* @return array<string, stdClass>
*
* @throws DatabaseException
*/
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 fn ($v) => trim($v), $_fields);
if (str_starts_with($row->indexdef, 'CREATE UNIQUE INDEX pk')) {
$obj->type = 'PRIMARY';
} else {
$obj->type = (str_starts_with($row->indexdef, 'CREATE UNIQUE')) ? 'UNIQUE' : 'INDEX';
}
$retVal[$obj->name] = $obj;
}
return $retVal;
}
/**
* Returns an array of objects with Foreign key data
*
* @return array<string, stdClass>
*
* @throws DatabaseException
*/
protected function _foreignKeyData(string $table): array
{
$sql = 'SELECT c.constraint_name,
x.table_name,
x.column_name,
y.table_name as foreign_table_name,
y.column_name as foreign_column_name,
c.delete_rule,
c.update_rule,
c.match_option
FROM information_schema.referential_constraints c
JOIN information_schema.key_column_usage x
on x.constraint_name = c.constraint_name
JOIN information_schema.key_column_usage y
on y.ordinal_position = x.position_in_unique_constraint
and y.constraint_name = c.unique_constraint_name
WHERE x.table_name = ' . $this->escape($table) .
'order by c.constraint_name, x.ordinal_position';
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetForeignKeyData'));
}
$query = $query->getResultObject();
$indexes = [];
foreach ($query as $row) {
$indexes[$row->constraint_name]['constraint_name'] = $row->constraint_name;
$indexes[$row->constraint_name]['table_name'] = $table;
$indexes[$row->constraint_name]['column_name'][] = $row->column_name;
$indexes[$row->constraint_name]['foreign_table_name'] = $row->foreign_table_name;
$indexes[$row->constraint_name]['foreign_column_name'][] = $row->foreign_column_name;
$indexes[$row->constraint_name]['on_delete'] = $row->delete_rule;
$indexes[$row->constraint_name]['on_update'] = $row->update_rule;
$indexes[$row->constraint_name]['match'] = $row->match_option;
}
return $this->foreignKeyDataToObjects($indexes);
}
/**
* 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 (str_contains($this->hostname, '/')) {
$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');
}
}
+220
View File
@@ -0,0 +1,220 @@
<?php
declare(strict_types=1);
/**
* 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';
/**
* @var Connection
*/
protected $db;
/**
* CREATE TABLE attributes
*
* @param array $attributes Associative array of table attributes
*/
protected function _createTableAttributes(array $attributes): string
{
return '';
}
/**
* @param array|string $processedFields Processed column definitions
* or column names to DROP
*
* @return false|list<string>|string SQL string or false
* @phpstan-return ($alterType is 'DROP' ? string : list<string>|false)
*/
protected function _alterTable(string $alterType, string $table, $processedFields)
{
if (in_array($alterType, ['DROP', 'ADD'], true)) {
return parent::_alterTable($alterType, $table, $processedFields);
}
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
$sqls = [];
foreach ($processedFields as $field) {
if ($field['_literal'] !== false) {
return false;
}
if (version_compare($this->db->getVersion(), '8', '>=') && isset($field['type'])) {
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name'])
. " TYPE {$field['type']}{$field['length']}";
}
if (! empty($field['default'])) {
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name'])
. " SET DEFAULT {$field['default']}";
}
$nullable = true; // Nullable by default.
if (isset($field['null']) && ($field['null'] === false || $field['null'] === ' NOT ' . $this->null)) {
$nullable = false;
}
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name'])
. ($nullable === true ? ' DROP' : ' SET') . ' NOT NULL';
if (! empty($field['new_name'])) {
$sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($field['name'])
. ' TO ' . $this->db->escapeIdentifiers($field['new_name']);
}
if (! empty($field['comment'])) {
$sqls[] = 'COMMENT ON COLUMN' . $this->db->escapeIdentifiers($table)
. '.' . $this->db->escapeIdentifiers($field['name'])
. " IS {$field['comment']}";
}
}
return $sqls;
}
/**
* Process column
*/
protected function _processColumn(array $processedField): string
{
return $this->db->escapeIdentifiers($processedField['name'])
. ' ' . $processedField['type'] . ($processedField['type'] === 'text' ? '' : $processedField['length'])
. $processedField['default']
. $processedField['null']
. $processedField['auto_increment']
. $processedField['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;
}
/**
* Constructs sql to check if key is a constraint.
*/
protected function _dropKeyAsConstraint(string $table, string $constraintName): string
{
return "SELECT con.conname
FROM pg_catalog.pg_constraint con
INNER JOIN pg_catalog.pg_class rel
ON rel.oid = con.conrelid
INNER JOIN pg_catalog.pg_namespace nsp
ON nsp.oid = connamespace
WHERE nsp.nspname = '{$this->db->schema}'
AND rel.relname = '" . trim($table, '"') . "'
AND con.conname = '" . trim($constraintName, '"') . "'";
}
}
+128
View File
@@ -0,0 +1,128 @@
<?php
declare(strict_types=1);
/**
* 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 CodeIgniter\Database\Exceptions\DatabaseException;
use Exception;
use PgSql\Connection as PgSqlConnection;
use PgSql\Result as PgSqlResult;
/**
* Prepared query for Postgre
*
* @extends BasePreparedQuery<PgSqlConnection, PgSqlResult, PgSqlResult>
*/
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 false|PgSqlResult
*/
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
*/
public function _prepare(string $sql, array $options = []): PreparedQuery
{
$this->name = (string) random_int(1, 10_000_000_000_000_000);
$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);
if ($this->db->DBDebug) {
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
}
}
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 or false on failure.
*
* @return PgSqlResult|null
*/
public function _getResult()
{
return $this->result;
}
/**
* Deallocate prepared statements.
*/
protected function _close(): bool
{
return pg_query($this->db->connID, 'DEALLOCATE "' . $this->db->escapeIdentifiers($this->name) . '"') !== false;
}
/**
* 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);
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
/**
* 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 PgSql\Connection as PgSqlConnection;
use PgSql\Result as PgSqlResult;
use stdClass;
/**
* Result for Postgre
*
* @extends BaseResult<PgSqlConnection, PgSqlResult>
*/
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.
*
* @return void
*/
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 bool
*/
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 array|false
*/
protected function fetchAssoc()
{
return pg_fetch_assoc($this->resultID);
}
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @return Entity|false|object|stdClass
*/
protected function fetchObject(string $className = 'stdClass')
{
if (is_subclass_of($className, Entity::class)) {
return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($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;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
/**
* 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 never
*/
public function _backup(?array $prefs = null)
{
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* 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;
/**
* @template TConnection
* @template TStatement
* @template TResult
*/
interface PreparedQueryInterface
{
/**
* Takes a new set of data and runs it against the currently
* prepared query. Upon success, will return a Results object.
*
* @return bool|ResultInterface
* @phpstan-return bool|ResultInterface<TConnection, TResult>
*/
public function execute(...$data);
/**
* Prepares the query against the database, and saves the connection
* info necessary to execute the query later.
*
* @return $this
*/
public function prepare(string $sql, array $options = []);
/**
* Explicity closes the statement.
*
* @throws BadMethodCallException
*/
public function close(): bool;
/**
* 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;
}
+431
View File
@@ -0,0 +1,431 @@
<?php
declare(strict_types=1);
/**
* 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 Stringable;
/**
* Query builder
*/
class Query implements QueryInterface, Stringable
{
/**
* The query string, as provided by the user.
*
* @var string
*/
protected $originalQueryString;
/**
* The query string if table prefix has been swapped.
*
* @var string|null
*/
protected $swappedQueryString;
/**
* The final query string after binding, etc.
*
* @var string|null
*/
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;
unset($this->swappedQueryString);
if ($binds !== null) {
if (! is_array($binds)) {
$binds = [$binds];
}
if ($setEscape) {
array_walk($binds, static function (&$item): void {
$item = [
$item,
true,
];
});
}
$this->binds = $binds;
}
unset($this->finalQueryString);
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): void {
$item = [$item, true];
});
}
$this->binds = $binds;
unset($this->finalQueryString);
return $this;
}
/**
* Returns the final, processed query string after binding, etal
* has been performed.
*/
public function getQuery(): string
{
if (empty($this->finalQueryString)) {
$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.
*
* @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 = $this->swappedQueryString ?? $this->originalQueryString;
$from = '/(\W)' . $orig . '(\S)/';
$to = '\\1' . $swap . '\\2';
$this->swappedQueryString = preg_replace($from, $to, $sql);
unset($this->finalQueryString);
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 property.
*
* @see https://regex101.com/r/EUEhay/5
*/
protected function compileBinds()
{
$sql = $this->swappedQueryString ?? $this->originalQueryString;
$binds = $this->binds;
if (empty($binds)) {
$this->finalQueryString = $sql;
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, (string) $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',
];
$sql = esc($this->getQuery());
/**
* @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 fn ($matches) => '<strong>' . str_replace(' ', '&nbsp;', $matches[0]) . '</strong>', $sql);
}
/**
* Return text representation of the query
*/
public function __toString(): string
{
return $this->getQuery();
}
}
+89
View File
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/**
* 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);
}
+56
View File
@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/**
* 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 Stringable;
/**
* @see \CodeIgniter\Database\RawSqlTest
*/
class RawSql implements Stringable
{
/**
* @var string Raw SQL string
*/
private string $string;
public function __construct(string $sqlString)
{
$this->string = $sqlString;
}
public function __toString(): string
{
return $this->string;
}
/**
* Create new instance with new SQL string
*/
public function with(string $newSqlString): self
{
$new = clone $this;
$new->string = $newSqlString;
return $new;
}
/**
* Returns unique id for binding key
*/
public function getBindingKey(): string
{
return 'RawSql' . spl_object_id($this);
}
}
+184
View File
@@ -0,0 +1,184 @@
<?php
declare(strict_types=1);
/**
* 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 stdClass;
/**
* @template TConnection
* @template TResult
*/
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 array
*/
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 the row doesn't exist, returns null.
*
* @template T of object
*
* @param int|string $n The index of the results to return, or column name.
* @param string $type The type of result object. 'array', 'object' or class name.
* @phpstan-param class-string<T>|'array'|'object' $type
*
* @return array|float|int|object|stdClass|string|null
* @phpstan-return ($n is string ? float|int|string|null : ($type is 'object' ? stdClass|null : ($type is 'array' ? array|null : T|null)))
*/
public function getRow($n = 0, string $type = 'object');
/**
* Returns a row as a custom class instance.
*
* If the row doesn't exist, returns null.
*
* @template T of object
*
* @param int $n The index of the results to return.
* @phpstan-param class-string<T> $className
*
* @return object|null
* @phpstan-return T|null
*/
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 array|null
*/
public function getRowArray(int $n = 0);
/**
* Returns a single row from the results as an object.
*
* If row doesn't exist, returns null.
*
* @return object|stdClass|null
*/
public function getRowObject(int $n = 0);
/**
* Assigns an item into a particular column slot.
*
* @param array|string $key
* @param array|object|stdClass|null $value
*
* @return void
*/
public function setRow($key, $value = null);
/**
* Returns the "first" row of the current results.
*
* @return array|object|null
*/
public function getFirstRow(string $type = 'object');
/**
* Returns the "last" row of the current results.
*
* @return array|object|null
*/
public function getLastRow(string $type = 'object');
/**
* Returns the "next" row of the current results.
*
* @return array|object|null
*/
public function getNextRow(string $type = 'object');
/**
* Returns the "previous" row of the current results.
*
* @return array|object|null
*/
public function getPreviousRow(string $type = 'object');
/**
* Returns number of rows in the result set.
*/
public function getNumRows(): int;
/**
* Returns an unbuffered row and move the pointer to the next row.
*
* @return array|object|null
*/
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 bool
*/
public function dataSeek(int $n = 0);
}
+821
View File
@@ -0,0 +1,821 @@
<?php
declare(strict_types=1);
/**
* 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\RawSql;
use CodeIgniter\Database\ResultInterface;
use Config\Feature;
/**
* 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[] = str_starts_with($value, '(SELECT') ? $value : $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
*
* @param RawSql|string $cond
*
* @return $this
*/
public function join(string $table, $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);
// Workaround for BETWEEN
if ($operator === false) {
$cond .= $joints[$i] . $condition;
continue;
}
$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
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$sql = 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $this->getFullName($table)
. ' (' . implode(', ', $keys) . ")\n{:_table_:}";
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = 'VALUES ' . implode(', ', $this->formatValues($values));
}
return str_replace('{:_table_:}', $data, $sql);
}
/**
* 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;
}
/**
* 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);
if (! $this->testMode) {
$this->resetWrite();
return $this->db->query($sql, $this->binds, false);
}
return true;
}
/**
* 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);
if (! $this->testMode) {
$this->resetWrite();
return $this->db->query($sql, $this->binds, false);
}
return true;
}
/**
* Get full name of the table
*/
private function getFullName(string $table): string
{
$alias = '';
if (str_contains($table, ' ')) {
$alias = explode(' ', $table);
$table = array_shift($alias);
$alias = ' ' . implode(' ', $alias);
}
if ($this->db->escapeChar === '"') {
if (str_contains($table, '.') && ! str_starts_with($table, '.') && ! str_ends_with($table, '.')) {
$dbInfo = explode('.', $table);
$database = $this->db->getDatabase();
$table = $dbInfo[0];
if (count($dbInfo) === 3) {
$database = str_replace('"', '', $dbInfo[0]);
$schema = str_replace('"', '', $dbInfo[1]);
$tableName = str_replace('"', '', $dbInfo[2]);
} else {
$schema = str_replace('"', '', $dbInfo[0]);
$tableName = str_replace('"', '', $dbInfo[1]);
}
return '"' . $database . '"."' . $schema . '"."' . str_replace('"', '', $tableName) . '"' . $alias;
}
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
{
// SQL Server cannot handle `LIMIT 0`.
// DatabaseException:
// [Microsoft][ODBC Driver 17 for SQL Server][SQL Server]The number of
// rows provided for a FETCH clause must be greater then zero.
$limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
if (! $limitZeroAsAll && $this->QBLimit === 0) {
return "SELECT * \nFROM " . $this->_fromTables() . ' WHERE 1=0 ';
}
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
*
* @return mixed
*
* @throws DatabaseException
*/
public function replace(?array $set = null)
{
if ($set !== null) {
$this->set($set);
}
if ($this->QBSet === []) {
if ($this->db->DBDebug) {
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(fn (string $field): string => $this->db->protectIdentifiers($field), array_values(array_unique($keyFields)));
// Get the binds
$binds = $this->binds;
array_walk($binds, static function (&$item): void {
$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 (str_contains($select, ',')) {
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
*
* @return mixed
*
* @throws DatabaseException
*/
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 ($this->QBWhere === []) {
if ($this->db->DBDebug) {
throw new DatabaseException('Deletes are not allowed unless they contain a "where" or "like" clause.');
}
return false; // @codeCoverageIgnore
}
if ($limit !== null && $limit !== 0) {
$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) && $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 ($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
$limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
if ($limitZeroAsAll) {
if ($this->QBLimit) {
$sql = $this->_limit($sql . "\n");
}
} elseif ($this->QBLimit !== false || $this->QBOffset) {
$sql = $this->_limit($sql . "\n");
}
return $this->unionInjection($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)
{
$limitZeroAsAll = config(Feature::class)->limitZeroAsAll ?? true;
if ($limitZeroAsAll && $limit === 0) {
$limit = null;
}
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;
}
/**
* Generates a platform-specific upsertBatch string from the supplied data
*
* @throws DatabaseException
*/
protected function _upsertBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$fullTableName = $this->getFullName($table);
$constraints = $this->QBOptions['constraints'] ?? [];
$tableIdentity = $this->QBOptions['tableIdentity'] ?? '';
$sql = "SELECT name from syscolumns where id = Object_ID('" . $table . "') and colstat = 1";
if (($query = $this->db->query($sql)) === false) {
throw new DatabaseException('Failed to get table identity');
}
$query = $query->getResultObject();
foreach ($query as $row) {
$tableIdentity = '"' . $row->name . '"';
}
$this->QBOptions['tableIdentity'] = $tableIdentity;
$identityInFields = in_array($tableIdentity, $keys, true);
$fieldNames = array_map(static fn ($columnName) => trim($columnName, '"'), $keys);
if (empty($constraints)) {
$tableIndexes = $this->db->getIndexData($table);
$uniqueIndexes = array_filter($tableIndexes, static function ($index) use ($fieldNames) {
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
return $index->type === 'PRIMARY' && $hasAllFields;
});
// if no primary found then look for unique - since indexes have no order
if ($uniqueIndexes === []) {
$uniqueIndexes = array_filter($tableIndexes, static function ($index) use ($fieldNames) {
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
return $index->type === 'UNIQUE' && $hasAllFields;
});
}
// only take first index
foreach ($uniqueIndexes as $index) {
$constraints = $index->fields;
break;
}
$constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? [];
}
if (empty($constraints)) {
if ($this->db->DBDebug) {
throw new DatabaseException('No constraint found for upsert.');
}
return ''; // @codeCoverageIgnore
}
$alias = $this->QBOptions['alias'] ?? '"_upsert"';
$updateFields = $this->QBOptions['updateFields'] ?? $this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ?? [];
$sql = 'MERGE INTO ' . $fullTableName . "\nUSING (\n";
$sql .= '{:_table_:}';
$sql .= ") {$alias} (";
$sql .= implode(', ', $keys);
$sql .= ')';
$sql .= "\nON (";
$sql .= implode(
' AND ',
array_map(
static fn ($key, $value) => (
($value instanceof RawSql && is_string($key))
?
$fullTableName . '.' . $key . ' = ' . $value
:
(
$value instanceof RawSql
?
$value
:
$fullTableName . '.' . $value . ' = ' . $alias . '.' . $value
)
),
array_keys($constraints),
$constraints
)
) . ")\n";
$sql .= "WHEN MATCHED THEN UPDATE SET\n";
$sql .= implode(
",\n",
array_map(
static fn ($key, $value) => $key . ($value instanceof RawSql ?
' = ' . $value :
" = {$alias}.{$value}"),
array_keys($updateFields),
$updateFields
)
);
$sql .= "\nWHEN NOT MATCHED THEN INSERT (" . implode(', ', $keys) . ")\nVALUES ";
$sql .= (
'(' . implode(
', ',
array_map(
static fn ($columnName) => $columnName === $tableIdentity
? "CASE WHEN {$alias}.{$columnName} IS NULL THEN (SELECT "
. 'isnull(IDENT_CURRENT(\'' . $fullTableName . '\')+IDENT_INCR(\''
. $fullTableName . "'),1)) ELSE {$alias}.{$columnName} END"
: "{$alias}.{$columnName}",
$keys
)
) . ');'
);
$sql = $identityInFields ? $this->addIdentity($fullTableName, $sql) : $sql;
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
/**
* Gets column names from a select query
*/
protected function fieldsFromQuery(string $sql): array
{
return $this->db->query('SELECT TOP 1 * FROM (' . $sql . ') _u_')->getFieldNames();
}
}
+569
View File
@@ -0,0 +1,569 @@
<?php
declare(strict_types=1);
/**
* 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 stdClass;
/**
* Connection for SQLSRV
*
* @extends BaseConnection<resource, resource>
*/
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 false|string
*/
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 list<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.
*
* @return false|resource
*
* @throws DatabaseException
*/
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']);
}
if (! str_contains($this->hostname, ',') && $this->port !== '') {
$this->hostname .= ', ' . $this->port;
}
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;
}
throw new DatabaseException($this->getAllErrorMessages());
}
/**
* For exception message
*
* @internal
*/
public function getAllErrorMessages(): string
{
$errors = [];
foreach (sqlsrv_errors() as $error) {
$errors[] = $error['message']
. ' SQLSTATE: ' . $error['SQLSTATE'] . ', code: ' . $error['code'];
}
return 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.
*
* @return void
*/
public function reconnect()
{
$this->close();
$this->initialize();
}
/**
* Close the database connection.
*
* @return void
*/
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 (int) ($this->query('SELECT SCOPE_IDENTITY() AS insert_id')->getRow()->insert_id ?? 0);
}
/**
* Generates the SQL for listing tables in a platform-dependent manner.
*
* @param string|null $tableName If $tableName is provided will return only this table if exists.
*/
protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
{
$sql = 'SELECT [TABLE_NAME] AS "name"'
. ' FROM [INFORMATION_SCHEMA].[TABLES] '
. ' WHERE '
. " [TABLE_SCHEMA] = '" . $this->schema . "' ";
if ($tableName !== null) {
return $sql .= ' AND [TABLE_NAME] LIKE ' . $this->escape($tableName);
}
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
*
* @return array<string, stdClass>
*
* @throws DatabaseException
*/
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 fn ($v) => trim($v), $_fields);
if (str_contains($row->index_description, 'primary key located on')) {
$obj->type = 'PRIMARY';
} else {
$obj->type = (str_contains($row->index_description, 'nonclustered, unique')) ? 'UNIQUE' : 'INDEX';
}
$retVal[$obj->name] = $obj;
}
return $retVal;
}
/**
* Returns an array of objects with Foreign key data
* referenced_object_id parent_object_id
*
* @return array<string, stdClass>
*
* @throws DatabaseException
*/
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,
rc.delete_rule,
rc.update_rule,
rc.match_option
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
INNER JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc ON rc.CONSTRAINT_NAME = f.name
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();
$indexes = [];
foreach ($query as $row) {
$indexes[$row->constraint_name]['constraint_name'] = $row->constraint_name;
$indexes[$row->constraint_name]['table_name'] = $row->table_name;
$indexes[$row->constraint_name]['column_name'][] = $row->column_name;
$indexes[$row->constraint_name]['foreign_table_name'] = $row->foreign_table_name;
$indexes[$row->constraint_name]['foreign_column_name'][] = $row->foreign_column_name;
$indexes[$row->constraint_name]['on_delete'] = $row->delete_rule;
$indexes[$row->constraint_name]['on_update'] = $row->update_rule;
$indexes[$row->constraint_name]['match'] = $row->match_option;
}
return $this->foreignKeyDataToObjects($indexes);
}
/**
* 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
*
* @return list<stdClass>
*
* @throws DatabaseException
*/
protected function _fieldData(string $table): array
{
$sql = 'SELECT
COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION,
COLUMN_DEFAULT, IS_NULLABLE
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]->max_length = $query[$i]->CHARACTER_MAXIMUM_LENGTH > 0
? $query[$i]->CHARACTER_MAXIMUM_LENGTH
: $query[$i]->NUMERIC_PRECISION;
$retVal[$i]->nullable = $query[$i]->IS_NULLABLE !== 'NO';
$retVal[$i]->default = $query[$i]->COLUMN_DEFAULT;
}
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 bool
*/
public function setDatabase(?string $databaseName = null)
{
if ($databaseName === null || $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 false|resource
*/
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 DatabaseException($error['message']);
}
}
return $stmt;
}
/**
* Returns the last error encountered by this connection.
*
* @return array<string, int|string>
*
* @deprecated Use `error()` instead.
*/
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
{
$info = [];
if (isset($this->dataCache['version'])) {
return $this->dataCache['version'];
}
if (! $this->connID || ($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 string $sql
*/
public function isWriteType($sql): bool
{
if (preg_match('/^\s*"?(EXEC\s*sp_rename)\s/i', $sql)) {
return true;
}
return parent::isWriteType($sql);
}
}
+448
View File
@@ -0,0 +1,448 @@
<?php
declare(strict_types=1);
/**
* 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 CodeIgniter\Database\Forge as BaseForge;
use Throwable;
/**
* 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 %s'\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',
];
/**
* Foreign Key Allowed Actions
*
* @var array
*/
protected $fkAllowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT'];
/**
* CREATE TABLE IF statement
*
* @var string
*
* @deprecated This is no longer used.
*/
protected $createTableIfStr;
/**
* CREATE TABLE statement
*
* @var string
*/
protected $createTableStr;
public function __construct(BaseConnection $db)
{
parent::__construct($db);
$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 database
*
* @param bool $ifNotExists Whether to add IF NOT EXISTS condition
*
* @throws DatabaseException
*/
public function createDatabase(string $dbName, bool $ifNotExists = false): bool
{
if ($ifNotExists) {
$sql = sprintf(
$this->createDatabaseIfStr,
$dbName,
$this->db->escapeIdentifier($dbName)
);
} else {
$sql = sprintf(
$this->createDatabaseStr,
$this->db->escapeIdentifier($dbName)
);
}
try {
if (! $this->db->query($sql)) {
// @codeCoverageIgnoreStart
if ($this->db->DBDebug) {
throw new DatabaseException('Unable to create the specified database.');
}
return false;
// @codeCoverageIgnoreEnd
}
if (isset($this->db->dataCache['db_names'])) {
$this->db->dataCache['db_names'][] = $dbName;
}
return true;
} catch (Throwable $e) {
if ($this->db->DBDebug) {
throw new DatabaseException('Unable to create the specified database.', 0, $e);
}
return false; // @codeCoverageIgnore
}
}
/**
* CREATE TABLE attributes
*/
protected function _createTableAttributes(array $attributes): string
{
return '';
}
/**
* @param array|string $processedFields Processed column definitions
* or column names to DROP
*
* @return false|list<string>|string SQL string or false
* @phpstan-return ($alterType is 'DROP' ? string : list<string>|false)
*/
protected function _alterTable(string $alterType, string $table, $processedFields)
{
// Handle DROP here
if ($alterType === 'DROP') {
$columnNamesToDrop = $processedFields;
// check if fields are part of any indexes
$indexData = $this->db->getIndexData($table);
foreach ($indexData as $index) {
if (is_string($columnNamesToDrop)) {
$columnNamesToDrop = explode(',', $columnNamesToDrop);
}
$fld = array_intersect($columnNamesToDrop, $index->fields);
// Drop index if field is part of an index
if ($fld !== []) {
$this->_dropIndex($table, $index);
}
}
$fullTable = $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table);
// Drop default constraints
$fields = implode(',', $this->db->escape((array) $columnNamesToDrop));
$sql = <<<SQL
SELECT name
FROM SYS.DEFAULT_CONSTRAINTS
WHERE PARENT_OBJECT_ID = OBJECT_ID('{$fullTable}')
AND PARENT_COLUMN_ID IN (
SELECT column_id FROM sys.columns WHERE NAME IN ({$fields}) AND object_id = OBJECT_ID(N'{$fullTable}')
)
SQL;
foreach ($this->db->query($sql)->getResultArray() as $index) {
$this->db->query('ALTER TABLE ' . $fullTable . ' DROP CONSTRAINT ' . $index['name'] . '');
}
$sql = 'ALTER TABLE ' . $fullTable . ' DROP ';
$fields = array_map(static fn ($item) => 'COLUMN [' . trim($item) . ']', (array) $columnNamesToDrop);
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 ($processedFields as $field) {
$sqls[] = $sql . ($field['_literal'] !== false ? $field['_literal'] : $this->_processColumn($field));
}
return $sqls;
}
foreach ($processedFields as $field) {
if ($field['_literal'] !== false) {
return false;
}
if (isset($field['type'])) {
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name'])
. " {$field['type']}{$field['length']}";
}
if (! empty($field['default'])) {
$sqls[] = $sql . ' ALTER COLUMN ADD CONSTRAINT ' . $this->db->escapeIdentifiers($field['name']) . '_def'
. " DEFAULT {$field['default']} FOR " . $this->db->escapeIdentifiers($field['name']);
}
$nullable = true; // Nullable by default.
if (isset($field['null']) && ($field['null'] === false || $field['null'] === ' NOT ' . $this->null)) {
$nullable = false;
}
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($field['name'])
. " {$field['type']}{$field['length']} " . ($nullable === true ? '' : 'NOT') . ' NULL';
if (! empty($field['comment'])) {
$sqls[] = 'EXEC sys.sp_addextendedproperty '
. "@name=N'Caption', @value=N'" . $field['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($field['name']) . "'";
}
if (! empty($field['new_name'])) {
$sqls[] = "EXEC sp_rename '[" . $this->db->schema . '].[' . $table . '].[' . $field['name'] . "]' , '" . $field['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);
}
/**
* Generates SQL to add indexes
*
* @param bool $asQuery When true returns stand alone SQL, else partial SQL used with CREATE TABLE
*/
protected function _processIndexes(string $table, bool $asQuery = false): array
{
$sqls = [];
for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
for ($i2 = 0, $c2 = count($this->keys[$i]['fields']); $i2 < $c2; $i2++) {
if (! isset($this->fields[$this->keys[$i]['fields'][$i2]])) {
unset($this->keys[$i]['fields'][$i2]);
}
}
if (count($this->keys[$i]['fields']) <= 0) {
continue;
}
$keyName = $this->db->escapeIdentifiers(($this->keys[$i]['keyName'] === '') ?
$table . '_' . implode('_', $this->keys[$i]['fields']) :
$this->keys[$i]['keyName']);
if (in_array($i, $this->uniqueKeys, true)) {
$sqls[] = 'ALTER TABLE '
. $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table)
. ' ADD CONSTRAINT ' . $keyName
. ' UNIQUE (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ');';
continue;
}
$sqls[] = 'CREATE INDEX '
. $keyName
. ' ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table)
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i]['fields'])) . ');';
}
return $sqls;
}
/**
* Process column
*/
protected function _processColumn(array $processedField): string
{
return $this->db->escapeIdentifiers($processedField['name'])
. (empty($processedField['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($processedField['new_name']))
. ' ' . $processedField['type'] . ($processedField['type'] === 'text' ? '' : $processedField['length'])
. $processedField['default']
. $processedField['null']
. $processedField['auto_increment']
. ''
. $processedField['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 'MEDIUMINT':
$attributes['TYPE'] = 'INTEGER';
$attributes['UNSIGNED'] = false;
break;
case 'INTEGER':
$attributes['TYPE'] = 'INT';
break;
case 'ENUM':
// in char(n) and varchar(n), the n defines the string length in
// bytes (0 to 8,000).
// https://learn.microsoft.com/en-us/sql/t-sql/data-types/char-and-varchar-transact-sql?view=sql-server-ver16#remarks
$maxLength = max(
array_map(
static fn ($value) => strlen($value),
$attributes['CONSTRAINT']
)
);
$attributes['TYPE'] = 'VARCHAR';
$attributes['CONSTRAINT'] = $maxLength;
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;
}
/**
* Constructs sql to check if key is a constraint.
*/
protected function _dropKeyAsConstraint(string $table, string $constraintName): string
{
return "SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE TABLE_NAME= '" . trim($table, '"') . "'
AND CONSTRAINT_NAME = '" . trim($constraintName, '"') . "'";
}
}
+137
View File
@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
/**
* 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 CodeIgniter\Database\Exceptions\DatabaseException;
/**
* Prepared query for Postgre
*
* @extends BasePreparedQuery<resource, resource, resource>
*/
class PreparedQuery extends BasePreparedQuery
{
/**
* Parameters array used to store the dynamic variables.
*
* @var array
*/
protected $parameters = [];
/**
* A reference to the db connection to use.
*
* @var Connection
*/
protected $db;
public function __construct(Connection $db)
{
parent::__construct($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.
*
* @param array $options Options takes an associative array;
*
* @throws DatabaseException
*/
public function _prepare(string $sql, array $options = []): PreparedQuery
{
// 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) {
if ($this->db->DBDebug) {
throw new DatabaseException($this->db->getAllErrorMessages());
}
$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.
*/
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;
}
$result = sqlsrv_execute($this->statement);
if ($result === false && $this->db->DBDebug) {
throw new DatabaseException($this->db->getAllErrorMessages());
}
return $result;
}
/**
* Returns the statement resource for the prepared query or false when preparing failed.
*
* @return resource|null
*/
public function _getResult()
{
return $this->statement;
}
/**
* Deallocate prepared statements.
*/
protected function _close(): bool
{
return sqlsrv_free_stmt($this->statement);
}
/**
* 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;
}
}
+176
View File
@@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
/**
* 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
*
* @extends BaseResult<resource, resource>
*/
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.
*
* @return void
*/
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 bool
*/
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 array|false|null
*/
protected function fetchAssoc()
{
return sqlsrv_fetch_array($this->resultID, SQLSRV_FETCH_ASSOC);
}
/**
* Returns the result set as an object.
*
* @return Entity|false|object|stdClass
*/
protected function fetchObject(string $className = 'stdClass')
{
if (is_subclass_of($className, Entity::class)) {
return empty($data = $this->fetchAssoc()) ? false : (new $className())->injectRawData($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;
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* 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 never
*/
public function _backup(?array $prefs = null)
{
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
}
+280
View File
@@ -0,0 +1,280 @@
<?php
declare(strict_types=1);
/**
* 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;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\RawSql;
use InvalidArgumentException;
/**
* 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;
}
/**
* Generates a platform-specific batch update string from the supplied data
*/
protected function _updateBatch(string $table, array $keys, array $values): string
{
if (version_compare($this->db->getVersion(), '3.33.0') >= 0) {
return parent::_updateBatch($table, $keys, $values);
}
$constraints = $this->QBOptions['constraints'] ?? [];
if ($constraints === []) {
if ($this->db->DBDebug) {
throw new DatabaseException('You must specify a constraint to match on for batch updates.');
}
return ''; // @codeCoverageIgnore
}
if (count($constraints) > 1 || isset($this->QBOptions['setQueryAsData']) || (current($constraints) instanceof RawSql)) {
throw new DatabaseException('You are trying to use a feature which requires SQLite version 3.33 or higher.');
}
$index = current($constraints);
$ids = [];
$final = [];
foreach ($values as $val) {
$val = array_combine($keys, $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') . $table . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere');
}
/**
* Generates a platform-specific upsertBatch string from the supplied data
*
* @throws DatabaseException
*/
protected function _upsertBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$constraints = $this->QBOptions['constraints'] ?? [];
if (empty($constraints)) {
$fieldNames = array_map(static fn ($columnName) => trim($columnName, '`'), $keys);
$allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames) {
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
return ($index->type === 'PRIMARY' || $index->type === 'UNIQUE') && $hasAllFields;
});
foreach (array_map(static fn ($index) => $index->fields, $allIndexes) as $index) {
$constraints[] = current($index);
break;
}
$constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? [];
}
if (empty($constraints)) {
if ($this->db->DBDebug) {
throw new DatabaseException('No constraint found for upsert.');
}
return ''; // @codeCoverageIgnore
}
$alias = $this->QBOptions['alias'] ?? '`excluded`';
if (strtolower($alias) !== '`excluded`') {
throw new InvalidArgumentException('SQLite alias is always named "excluded". A custom alias cannot be used.');
}
$updateFields = $this->QBOptions['updateFields'] ??
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
[];
$sql = 'INSERT INTO ' . $table . ' (';
$sql .= implode(', ', array_map(static fn ($columnName) => $columnName, $keys));
$sql .= ")\n";
$sql .= '{:_table_:}';
$sql .= 'ON CONFLICT(' . implode(',', $constraints) . ")\n";
$sql .= "DO UPDATE SET\n";
$sql .= implode(
",\n",
array_map(
static fn ($key, $value) => $key . ($value instanceof RawSql ?
" = {$value}" :
" = {$alias}.{$value}"),
array_keys($updateFields),
$updateFields
)
);
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$hasWhere = stripos($this->QBOptions['setQueryAsData'], 'WHERE') > 0;
$data = $this->QBOptions['setQueryAsData'] . ($hasWhere ? '' : "\nWHERE 1 = 1\n");
} else {
$data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
/**
* Generates a platform-specific batch update string from the supplied data
*/
protected function _deleteBatch(string $table, array $keys, array $values): string
{
$sql = $this->QBOptions['sql'] ?? '';
// if this is the first iteration of batch then we need to build skeleton sql
if ($sql === '') {
$constraints = $this->QBOptions['constraints'] ?? [];
if ($constraints === []) {
if ($this->db->DBDebug) {
throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore
}
return ''; // @codeCoverageIgnore
}
$sql = 'DELETE FROM ' . $table . "\n";
if (current($constraints) instanceof RawSql && $this->db->DBDebug) {
throw new DatabaseException('You cannot use RawSql for constraint in SQLite.');
// @codeCoverageIgnore
}
if (is_string(current(array_keys($constraints)))) {
$concat1 = implode(' || ', array_keys($constraints));
$concat2 = implode(' || ', array_values($constraints));
} else {
$concat1 = implode(' || ', $constraints);
$concat2 = $concat1;
}
$sql .= "WHERE {$concat1} IN (SELECT {$concat2} FROM (\n{:_table_:}))";
// where is not supported
if ($this->QBWhere !== [] && $this->db->DBDebug) {
throw new DatabaseException('You cannot use WHERE with SQLite.');
// @codeCoverageIgnore
}
$this->QBOptions['sql'] = $sql;
}
if (isset($this->QBOptions['setQueryAsData'])) {
$data = $this->QBOptions['setQueryAsData'];
} else {
$data = implode(
" UNION ALL\n",
array_map(
static fn ($value) => 'SELECT ' . implode(', ', array_map(
static fn ($key, $index) => $index . ' ' . $key,
$keys,
$value
)),
$values
)
) . "\n";
}
return str_replace('{:_table_:}', $data, $sql);
}
}
+460
View File
@@ -0,0 +1,460 @@
<?php
declare(strict_types=1);
/**
* 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 Exception;
use SQLite3;
use SQLite3Result;
use stdClass;
/**
* Connection for SQLite3
*
* @extends BaseConnection<SQLite3, SQLite3Result>
*/
class Connection extends BaseConnection
{
/**
* Database driver
*
* @var string
*/
public $DBDriver = 'SQLite3';
/**
* Identifier escape character
*
* @var string
*/
public $escapeChar = '`';
/**
* @var bool Enable Foreign Key constraint or not
*/
protected $foreignKeys = false;
/**
* The milliseconds to sleep
*
* @var int|null milliseconds
*
* @see https://www.php.net/manual/en/sqlite3.busytimeout
*/
protected $busyTimeout;
/**
* @return void
*/
public function initialize()
{
parent::initialize();
if ($this->foreignKeys) {
$this->enableForeignKeyChecks();
}
if (is_int($this->busyTimeout)) {
$this->connID->busyTimeout($this->busyTimeout);
}
}
/**
* Connect to the database.
*
* @return SQLite3
*
* @throws DatabaseException
*/
public function connect(bool $persistent = false)
{
if ($persistent && $this->DBDebug) {
throw new DatabaseException('SQLite3 doesn\'t support persistent connections.');
}
try {
if ($this->database !== ':memory:' && ! str_contains($this->database, DIRECTORY_SEPARATOR)) {
$this->database = WRITEPATH . $this->database;
}
$sqlite = (! $this->password)
? new SQLite3($this->database)
: new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
$sqlite->enableExceptions(true);
return $sqlite;
} 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.
*
* @return void
*/
public function reconnect()
{
$this->close();
$this->initialize();
}
/**
* Close the database connection.
*
* @return void
*/
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 false|SQLite3Result
*/
protected function execute(string $sql)
{
try {
return $this->isWriteType($sql)
? $this->connID->exec($sql)
: $this->connID->query($sql);
} catch (Exception $e) {
log_message('error', (string) $e);
if ($this->DBDebug) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $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
{
if (! $this->connID instanceof SQLite3) {
$this->initialize();
}
return $this->connID->escapeString($str);
}
/**
* Generates the SQL for listing tables in a platform-dependent manner.
*
* @param string|null $tableName If $tableName is provided will return only this table if exists.
*/
protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
{
if ($tableName !== null) {
return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
. ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
. ' AND "NAME" LIKE ' . $this->escape($tableName);
}
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) . ')';
}
/**
* @return false|list<string>
*
* @throws DatabaseException
*/
public function getFieldNames(string $table)
{
// Is there a cached result?
if (isset($this->dataCache['field_names'][$table])) {
return $this->dataCache['field_names'][$table];
}
if (! $this->connID instanceof SQLite3) {
$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
*
* @return list<stdClass>
*
* @throws DatabaseException
*/
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]->nullable = isset($query[$i]->notnull) && ! (bool) $query[$i]->notnull;
$retVal[$i]->default = $query[$i]->dflt_value;
// "pk" (either zero for columns that are not part of the primary key,
// or the 1-based index of the column within the primary key).
// https://www.sqlite.org/pragma.html#pragma_table_info
$retVal[$i]->primary_key = ($query[$i]->pk === 0) ? 0 : 1;
}
return $retVal;
}
/**
* Returns an array of objects with index data
*
* @return array<string, stdClass>
*
* @throws DatabaseException
*/
protected function _indexData(string $table): array
{
$sql = "SELECT 'PRIMARY' as indexname, l.name as fieldname, 'PRIMARY' as indextype
FROM pragma_table_info(" . $this->escape(strtolower($table)) . ") as l
WHERE l.pk <> 0
UNION ALL
SELECT sqlite_master.name as indexname, ii.name as fieldname,
CASE
WHEN ti.pk <> 0 AND sqlite_master.name LIKE 'sqlite_autoindex_%' THEN 'PRIMARY'
WHEN sqlite_master.name LIKE 'sqlite_autoindex_%' THEN 'UNIQUE'
WHEN sqlite_master.sql LIKE '% UNIQUE %' THEN 'UNIQUE'
ELSE 'INDEX'
END as indextype
FROM sqlite_master
INNER JOIN pragma_index_xinfo(sqlite_master.name) ii ON ii.name IS NOT NULL
LEFT JOIN pragma_table_info(" . $this->escape(strtolower($table)) . ") ti ON ti.name = ii.name
WHERE sqlite_master.type='index' AND sqlite_master.tbl_name = " . $this->escape(strtolower($table)) . ' COLLATE NOCASE';
if (($query = $this->query($sql)) === false) {
throw new DatabaseException(lang('Database.failGetIndexData'));
}
$query = $query->getResultObject();
$tempVal = [];
foreach ($query as $row) {
if ($row->indextype === 'PRIMARY') {
$tempVal['PRIMARY']['indextype'] = $row->indextype;
$tempVal['PRIMARY']['indexname'] = $row->indexname;
$tempVal['PRIMARY']['fields'][$row->fieldname] = $row->fieldname;
} else {
$tempVal[$row->indexname]['indextype'] = $row->indextype;
$tempVal[$row->indexname]['indexname'] = $row->indexname;
$tempVal[$row->indexname]['fields'][$row->fieldname] = $row->fieldname;
}
}
$retVal = [];
foreach ($tempVal as $val) {
$obj = new stdClass();
$obj->name = $val['indexname'];
$obj->fields = array_values($val['fields']);
$obj->type = $val['indextype'];
$retVal[$obj->name] = $obj;
}
return $retVal;
}
/**
* Returns an array of objects with Foreign key data
*
* @return array<string, stdClass>
*/
protected function _foreignKeyData(string $table): array
{
if ($this->supportsForeignKeys() !== true) {
return [];
}
$query = $this->query("PRAGMA foreign_key_list({$table})")->getResult();
$indexes = [];
foreach ($query as $row) {
$indexes[$row->id]['constraint_name'] = null;
$indexes[$row->id]['table_name'] = $table;
$indexes[$row->id]['foreign_table_name'] = $row->table;
$indexes[$row->id]['column_name'][] = $row->from;
$indexes[$row->id]['foreign_column_name'][] = $row->to;
$indexes[$row->id]['on_delete'] = $row->on_delete;
$indexes[$row->id]['on_update'] = $row->on_update;
$indexes[$row->id]['match'] = $row->match;
}
return $this->foreignKeyDataToObjects($indexes);
}
/**
* 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;
}
}
+324
View File
@@ -0,0 +1,324 @@
<?php
declare(strict_types=1);
/**
* 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->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 array|string $processedFields Processed column definitions
* or column names to DROP
*
* @return array|string|null
* @return list<string>|string|null SQL string or null
* @phpstan-return ($alterType is 'DROP' ? string : list<string>|null)
*/
protected function _alterTable(string $alterType, string $table, $processedFields)
{
switch ($alterType) {
case 'DROP':
$columnNamesToDrop = $processedFields;
$sqlTable = new Table($this->db, $this);
$sqlTable->fromTable($table)
->dropColumn($columnNamesToDrop)
->run();
return ''; // Why empty string?
case 'CHANGE':
$fieldsToModify = [];
foreach ($processedFields as $processedField) {
$name = $processedField['name'];
$newName = $processedField['new_name'];
$field = $this->fields[$name];
$field['name'] = $name;
$field['new_name'] = $newName;
// Unlike when creating a table, if `null` is not specified,
// the column will be `NULL`, not `NOT NULL`.
if ($processedField['null'] === '') {
$field['null'] = true;
}
$fieldsToModify[] = $field;
}
(new Table($this->db, $this))
->fromTable($table)
->modifyColumn($fieldsToModify)
->run();
return null; // Why null?
default:
return parent::_alterTable($alterType, $table, $processedFields);
}
}
/**
* Process column
*/
protected function _processColumn(array $processedField): string
{
if ($processedField['type'] === 'TEXT' && str_starts_with($processedField['length'], "('")) {
$processedField['type'] .= ' CHECK(' . $this->db->escapeIdentifiers($processedField['name'])
. ' IN ' . $processedField['length'] . ')';
}
return $this->db->escapeIdentifiers($processedField['name'])
. ' ' . $processedField['type']
. $processedField['auto_increment']
. $processedField['null']
. $processedField['unique']
. $processedField['default'];
}
/**
* 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();
}
/**
* Drop Primary Key
*/
public function dropPrimaryKey(string $table, string $keyName = ''): bool
{
$sqlTable = new Table($this->db, $this);
return $sqlTable->fromTable($this->db->DBPrefix . $table)
->dropPrimaryKey()
->run();
}
public function addForeignKey($fieldName = '', string $tableName = '', $tableField = '', string $onUpdate = '', string $onDelete = '', string $fkName = ''): BaseForge
{
if ($fkName === '') {
return parent::addForeignKey($fieldName, $tableName, $tableField, $onUpdate, $onDelete, $fkName);
}
throw new DatabaseException('SQLite does not support foreign key names. CodeIgniter will refer to them in the format: prefix_table_column_referencecolumn_foreign');
}
/**
* Generates SQL to add primary key
*
* @param bool $asQuery When true recreates table with key, else partial SQL used with CREATE TABLE
*/
protected function _processPrimaryKeys(string $table, bool $asQuery = false): string
{
if ($asQuery === false) {
return parent::_processPrimaryKeys($table, $asQuery);
}
$sqlTable = new Table($this->db, $this);
$sqlTable->fromTable($this->db->DBPrefix . $table)
->addPrimaryKey($this->primaryKeys)
->run();
return '';
}
/**
* Generates SQL to add foreign keys
*
* @param bool $asQuery When true recreates table with key, else partial SQL used with CREATE TABLE
*/
protected function _processForeignKeys(string $table, bool $asQuery = false): array
{
if ($asQuery === false) {
return parent::_processForeignKeys($table, $asQuery);
}
$errorNames = [];
foreach ($this->foreignKeys as $name) {
foreach ($name['field'] as $f) {
if (! isset($this->fields[$f])) {
$errorNames[] = $f;
}
}
}
if ($errorNames !== []) {
$errorNames = [implode(', ', $errorNames)];
throw new DatabaseException(lang('Database.fieldNotExists', $errorNames));
}
$sqlTable = new Table($this->db, $this);
$sqlTable->fromTable($this->db->DBPrefix . $table)
->addForeignKey($this->foreignKeys)
->run();
return [];
}
}
+116
View File
@@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
/**
* 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;
use CodeIgniter\Database\Exceptions\DatabaseException;
use Exception;
use SQLite3;
use SQLite3Result;
use SQLite3Stmt;
/**
* Prepared query for SQLite3
*
* @extends BasePreparedQuery<SQLite3, SQLite3Stmt, SQLite3Result>
*/
class PreparedQuery extends BasePreparedQuery
{
/**
* The SQLite3Result resource, or false.
*
* @var false|SQLite3Result
*/
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.
*/
public function _prepare(string $sql, array $options = []): PreparedQuery
{
if (! ($this->statement = $this->db->connID->prepare($sql))) {
$this->errorCode = $this->db->connID->lastErrorCode();
$this->errorString = $this->db->connID->lastErrorMsg();
if ($this->db->DBDebug) {
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
}
}
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 => $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);
}
try {
$this->result = $this->statement->execute();
} catch (Exception $e) {
if ($this->db->DBDebug) {
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
}
return false;
}
return $this->result !== false;
}
/**
* Returns the result object for the prepared query or false on failure.
*
* @return false|SQLite3Result
*/
public function _getResult()
{
return $this->result;
}
/**
* Deallocate prepared statements.
*/
protected function _close(): bool
{
return $this->statement->close();
}
}
+160
View File
@@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
/**
* 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 SQLite3;
use SQLite3Result;
use stdClass;
/**
* Result for SQLite3
*
* @extends BaseResult<SQLite3, SQLite3Result>
*/
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.
*
* @return void
*/
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.
*
* @return bool
*
* @throws DatabaseException
*/
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 array|false
*/
protected function fetchAssoc()
{
return $this->resultID->fetchArray(SQLITE3_ASSOC);
}
/**
* Returns the result set as an object.
*
* Overridden by child classes.
*
* @return Entity|false|object|stdClass
*/
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->injectRawData($row);
}
$classSet = Closure::bind(function ($key, $value): void {
$this->{$key} = $value;
}, $classObj, $className);
foreach (array_keys($row) as $key) {
$classSet($key, $row[$key]);
}
return $classObj;
}
}
+490
View File
@@ -0,0 +1,490 @@
<?php
declare(strict_types=1);
/**
* 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;
use stdClass;
/**
* 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<string, array<string, bool|int|string|null>> [name => attributes]
*/
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) && str_starts_with($table, $prefix)) {
$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)));
// if primary key index exists twice then remove psuedo index name 'primary'.
$primaryIndexes = array_filter($this->keys, static fn ($index) => $index['type'] === 'primary');
if ($primaryIndexes !== [] && count($primaryIndexes) > 1 && array_key_exists('primary', $this->keys)) {
unset($this->keys['primary']);
}
$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 list<string>|string $columns Column names to drop.
*
* @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.
*
* @param list<array<string, bool|int|string|null>> $fieldsToModify
*
* @return Table
*/
public function modifyColumn(array $fieldsToModify)
{
foreach ($fieldsToModify as $field) {
$oldName = $field['name'];
unset($field['name']);
$this->fields[$oldName] = $field;
}
return $this;
}
/**
* Drops the primary key
*/
public function dropPrimaryKey(): Table
{
$primaryIndexes = array_filter($this->keys, static fn ($index) => strtolower($index['type']) === 'primary');
foreach (array_keys($primaryIndexes) as $key) {
unset($this->keys[$key]);
}
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 $foreignName)
{
if (empty($this->foreignKeys)) {
return $this;
}
if (isset($this->foreignKeys[$foreignName])) {
unset($this->foreignKeys[$foreignName]);
}
return $this;
}
/**
* Adds primary key
*/
public function addPrimaryKey(array $fields): Table
{
$primaryIndexes = array_filter($this->keys, static fn ($index) => strtolower($index['type']) === 'primary');
// if primary key already exists we can't add another one
if ($primaryIndexes !== []) {
return $this;
}
// add array to keys of fields
$pk = [
'fields' => $fields['fields'],
'type' => 'primary',
];
$this->keys['primary'] = $pk;
return $this;
}
/**
* Add a foreign key
*
* @return $this
*/
public function addForeignKey(array $foreignKeys)
{
$fk = [];
// convert to object
foreach ($foreignKeys as $row) {
$obj = new stdClass();
$obj->column_name = $row['field'];
$obj->foreign_table_name = $row['referenceTable'];
$obj->foreign_column_name = $row['referenceField'];
$obj->on_delete = $row['onDelete'];
$obj->on_update = $row['onUpdate'];
$fk[] = $obj;
}
$this->foreignKeys = array_merge($this->foreignKeys, $fk);
return $this;
}
/**
* Creates the new table based on our current fields.
*
* @return bool
*/
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);
$fieldNames = array_keys($fields);
$this->keys = array_filter(
$this->keys,
static fn ($index) => count(array_intersect($index['fields'], $fieldNames)) === count($index['fields'])
);
// Unique/Index keys
if (is_array($this->keys)) {
foreach ($this->keys as $keyName => $key) {
switch ($key['type']) {
case 'primary':
$this->forge->addPrimaryKey($key['fields']);
break;
case 'unique':
$this->forge->addUniqueKey($key['fields'], $keyName);
break;
case 'index':
$this->forge->addKey($key['fields'], false, false, $keyName);
break;
}
}
}
foreach ($this->foreignKeys as $foreignKey) {
$this->forge->addForeignKey(
$foreignKey->column_name,
trim($foreignKey->foreign_table_name, $this->db->DBPrefix),
$foreignKey->foreign_column_name
);
}
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(
', ',
array_map(fn ($item) => $this->db->protectIdentifiers($item), $exFields)
);
$newFields = implode(
', ',
array_map(fn ($item) => $this->db->protectIdentifiers($item), $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
* @phpstan-return ($fields is array ? array : 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->default === null) {
// `null` means that the default value is not defined.
unset($return[$field->name]['default']);
} elseif ($field->default === 'NULL') {
// 'NULL' means that the default value is NULL.
$return[$field->name]['default'] = null;
} else {
$default = trim($field->default, "'");
if ($this->isIntegerType($field->type)) {
$default = (int) $default;
} elseif ($this->isNumericType($field->type)) {
$default = (float) $default;
}
$return[$field->name]['default'] = $default;
}
if ($field->primary_key) {
$this->keys['primary'] = [
'fields' => [$field->name],
'type' => 'primary',
];
}
}
return $return;
}
/**
* Is INTEGER type?
*
* @param string $type SQLite data type (case-insensitive)
*
* @see https://www.sqlite.org/datatype3.html
*/
private function isIntegerType(string $type): bool
{
return str_contains(strtoupper($type), 'INT');
}
/**
* Is NUMERIC type?
*
* @param string $type SQLite data type (case-insensitive)
*
* @see https://www.sqlite.org/datatype3.html
*/
private function isNumericType(string $type): bool
{
return in_array(strtoupper($type), ['NUMERIC', 'DECIMAL'], true);
}
/**
* Converts keys retrieved from the database to
* the format needed to create later.
*
* @param array<string, stdClass> $keys
*
* @return array<string, array{fields: string, type: string}>
*/
protected function formatKeys($keys)
{
$return = [];
foreach ($keys as $name => $key) {
$return[strtolower($name)] = [
'fields' => $key->fields,
'type' => strtolower($key->type),
];
}
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 (array_keys($this->keys) as $name) {
if ($name === 'primary') {
continue;
}
$this->db->query("DROP INDEX IF EXISTS '{$name}'");
}
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
/**
* 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 never
*/
public function _backup(?array $prefs = null)
{
throw new DatabaseException('Unsupported feature of the database platform you are using.');
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
declare(strict_types=1);
/**
* 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 non-empty-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.
*
* @deprecated
*/
private static ?Generator $faker = null;
/**
* Seeder constructor.
*/
public function __construct(Database $config, ?BaseConnection $db = null)
{
$this->seedPath = $config->filesPath ?? APPPATH . 'Database/';
if ($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 ??= 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 (! str_contains($class, '\\')) {
$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 void
*
* @codeCoverageIgnore
*/
public function run()
{
}
}