add trashes
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,270 @@
|
||||
<?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 CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the input contain binary data.
|
||||
*/
|
||||
protected function isBinary(string $input): bool
|
||||
{
|
||||
return mb_detect_encoding($input, 'UTF-8', true) === false;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 CodeIgniter\Exceptions\InvalidArgumentException;
|
||||
use Config\Database as DbConfig;
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected static function ensureFactory()
|
||||
{
|
||||
if (static::$factory instanceof Database) {
|
||||
return;
|
||||
}
|
||||
|
||||
static::$factory = new Database();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 CodeIgniter\Exceptions\ConfigException;
|
||||
use CodeIgniter\Exceptions\CriticalError;
|
||||
use CodeIgniter\Exceptions\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.');
|
||||
}
|
||||
|
||||
assert($this->checkDbExtension($params['DBDriver']));
|
||||
|
||||
$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 === 0 || $dsn === '' || $dsn === '0' || $dsn === [] || $dsn === false || $dsn === null) {
|
||||
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|ConnectionInterface $argument The constructor parameter or DB connection
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the PHP database extension is loaded.
|
||||
*
|
||||
* @param string $driver DB driver or FQCN for custom driver
|
||||
*/
|
||||
private function checkDbExtension(string $driver): bool
|
||||
{
|
||||
if (str_contains($driver, '\\')) {
|
||||
// Cannot check a fully qualified classname for a custom driver.
|
||||
return true;
|
||||
}
|
||||
|
||||
$extensionMap = [
|
||||
// DBDriver => PHP extension
|
||||
'MySQLi' => 'mysqli',
|
||||
'SQLite3' => 'sqlite3',
|
||||
'Postgre' => 'pgsql',
|
||||
'SQLSRV' => 'sqlsrv',
|
||||
'OCI8' => 'oci8',
|
||||
];
|
||||
|
||||
$extension = $extensionMap[$driver] ?? '';
|
||||
|
||||
if ($extension === '') {
|
||||
$message = 'Invalid DBDriver name: "' . $driver . '"';
|
||||
|
||||
throw new ConfigException($message);
|
||||
}
|
||||
|
||||
if (extension_loaded($extension)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$message = 'The required PHP extension "' . $extension . '" is not loaded.'
|
||||
. ' Install and enable it to use "' . $driver . '" driver.';
|
||||
|
||||
throw new CriticalError($message);
|
||||
}
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
<?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 CodeIgniter\Exceptions\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]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forInvalidAllowedFields(string $model)
|
||||
{
|
||||
return new static(lang('Database.invalidAllowedFields', [$model]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forTableNotFound(string $table)
|
||||
{
|
||||
return new static(lang('Database.tableNotFound', [$table]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forEmptyInputGiven(string $argument)
|
||||
{
|
||||
return new static(lang('Database.forEmptyInputGiven', [$argument]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forFindColumnHaveMultipleColumns()
|
||||
{
|
||||
return new static(lang('Database.forFindColumnHaveMultipleColumns'));
|
||||
}
|
||||
}
|
||||
+25
@@ -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 CodeIgniter\Exceptions\RuntimeException;
|
||||
|
||||
class DatabaseException extends RuntimeException implements ExceptionInterface, HasExitCodeInterface
|
||||
{
|
||||
public function getExitCode(): int
|
||||
{
|
||||
return EXIT_DATABASE;
|
||||
}
|
||||
}
|
||||
+24
@@ -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
@@ -0,0 +1,78 @@
|
||||
<?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 instanceof Forge) {
|
||||
$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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function up();
|
||||
|
||||
/**
|
||||
* Revert a migration step.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
abstract public function down();
|
||||
}
|
||||
@@ -0,0 +1,880 @@
|
||||
<?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\Exceptions\RuntimeException;
|
||||
use CodeIgniter\I18n\Time;
|
||||
use Config\Database;
|
||||
use Config\Migrations as MigrationsConfig;
|
||||
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
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
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 !== null ? [$this->namespace] : array_keys(service('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 = service('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) !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$locator = service('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 $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 $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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function clearHistory()
|
||||
{
|
||||
if ($this->db->tableExists($this->table)) {
|
||||
$this->db->table($this->table)->truncate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a history to the table.
|
||||
*
|
||||
* @param object $migration
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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 !== null) {
|
||||
$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) && $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 $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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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): string => $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): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $index . ' ' . $key,
|
||||
$keys,
|
||||
$value,
|
||||
)),
|
||||
$values,
|
||||
),
|
||||
) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
<?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 CodeIgniter\Database\TableName;
|
||||
use CodeIgniter\Exceptions\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 $connId.
|
||||
*
|
||||
* @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;
|
||||
|
||||
/**
|
||||
* Use MYSQLI_CLIENT_FOUND_ROWS
|
||||
*
|
||||
* Whether affectedRows() should return number of rows found,
|
||||
* or number of rows changed, after an UPDATE query.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $foundRows = 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 ? '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;
|
||||
}
|
||||
|
||||
if ($this->foundRows) {
|
||||
$clientFlags += MYSQLI_CLIENT_FOUND_ROWS;
|
||||
}
|
||||
|
||||
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) !== 0 && 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 ((string) $tableName !== '') {
|
||||
return $sql . ' LIKE ' . $this->escape($tableName);
|
||||
}
|
||||
|
||||
if ($prefixLimit && $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.
|
||||
*
|
||||
* @param string|TableName $table
|
||||
*/
|
||||
protected function _listColumns($table = ''): string
|
||||
{
|
||||
$tableName = $this->protectIdentifiers(
|
||||
$table,
|
||||
true,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
||||
return 'SHOW COLUMNS FROM ' . $tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'));
|
||||
}
|
||||
|
||||
$indexes = $query->getResultArray();
|
||||
|
||||
if ($indexes === []) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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 !== '' && ! str_contains($sql, 'CHARACTER SET') && ! str_contains($sql, 'CHARSET')) {
|
||||
$sql .= ' DEFAULT CHARACTER SET = ' . $this->db->escapeString($this->db->charset);
|
||||
}
|
||||
|
||||
if ($this->db->DBCollat !== '' && ! str_contains($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) {
|
||||
$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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
<?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\BasePreparedQuery;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
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 = '';
|
||||
$binaryData = [];
|
||||
|
||||
// Determine the type string
|
||||
foreach ($data as $key => $item) {
|
||||
if (is_int($item)) {
|
||||
$bindTypes .= 'i';
|
||||
} elseif (is_numeric($item)) {
|
||||
$bindTypes .= 'd';
|
||||
} elseif (is_string($item) && $this->isBinary($item)) {
|
||||
$bindTypes .= 'b';
|
||||
$binaryData[$key] = $item;
|
||||
} else {
|
||||
$bindTypes .= 's';
|
||||
}
|
||||
}
|
||||
|
||||
// Bind it
|
||||
$this->statement->bind_param($bindTypes, ...$data);
|
||||
|
||||
// Stream binary data
|
||||
foreach ($binaryData as $key => $value) {
|
||||
$this->statement->send_long_data($key, $value);
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
<?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) ';
|
||||
|
||||
/**
|
||||
* 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): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $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): string => trim($columnName, '"'), $keys);
|
||||
|
||||
$uniqueIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames): bool {
|
||||
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
|
||||
|
||||
return ($index->type === 'PRIMARY') && $hasAllFields;
|
||||
});
|
||||
$replaceableFields = array_filter($keys, static function ($columnName) use ($uniqueIndexes): bool {
|
||||
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): string => $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): string => $table . '."' . $columnName . '" = "_replace"."' . $columnName . '"', $index->fields)) . ')';
|
||||
}
|
||||
|
||||
$sql .= implode(' OR ', $onList) . ') WHEN MATCHED THEN UPDATE SET ';
|
||||
|
||||
$sql .= implode(', ', array_map(static fn ($columnName): string => $columnName . ' = "_replace".' . $columnName, $replaceableFields));
|
||||
|
||||
$sql .= ' WHEN NOT MATCHED THEN INSERT (' . implode(', ', $replaceableFields) . ') VALUES ';
|
||||
|
||||
return $sql . (' (' . implode(', ', array_map(static fn ($columnName): string => '"_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);
|
||||
|
||||
// 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';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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): string => $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): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $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): string => trim($columnName, '"'), $keys);
|
||||
|
||||
$uniqueIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames): bool {
|
||||
$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): string => $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): string => "{$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): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $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): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,757 @@
|
||||
<?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 CodeIgniter\Database\TableName;
|
||||
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 === false) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
if (($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 && $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.
|
||||
*
|
||||
* @param string|TableName $table
|
||||
*/
|
||||
protected function _listColumns($table = ''): string
|
||||
{
|
||||
if ($table instanceof TableName) {
|
||||
$tableName = $this->escape(strtoupper($table->getActualTableName()));
|
||||
$owner = $this->username;
|
||||
} elseif (str_contains($table, '.')) {
|
||||
sscanf($table, '%[^.].%s', $owner, $tableName);
|
||||
$tableName = $this->escape(strtoupper($this->DBPrefix . $tableName));
|
||||
} else {
|
||||
$owner = $this->username;
|
||||
$tableName = $this->escape(strtoupper($this->DBPrefix . $table));
|
||||
}
|
||||
|
||||
return 'SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS
|
||||
WHERE UPPER(OWNER) = ' . $this->escape(strtoupper($owner)) . '
|
||||
AND UPPER(TABLE_NAME) = ' . $tableName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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_';
|
||||
}
|
||||
}
|
||||
@@ -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 && $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
|
||||
&& str_contains(strtolower($field['type']), 'number')
|
||||
&& 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) {
|
||||
$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, '"') . "'";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
<?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\BasePreparedQuery;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
use OCILob;
|
||||
|
||||
/**
|
||||
* 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.');
|
||||
}
|
||||
|
||||
$binaryData = null;
|
||||
|
||||
foreach (array_keys($data) as $key) {
|
||||
if (is_string($data[$key]) && $this->isBinary($data[$key])) {
|
||||
$binaryData = oci_new_descriptor($this->db->connID, OCI_D_LOB);
|
||||
$binaryData->writeTemporary($data[$key], OCI_TEMP_BLOB);
|
||||
oci_bind_by_name($this->statement, ':' . $key, $binaryData, -1, OCI_B_BLOB);
|
||||
} else {
|
||||
oci_bind_by_name($this->statement, ':' . $key, $data[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
$result = oci_execute($this->statement, $this->db->commitMode);
|
||||
|
||||
if ($binaryData instanceof OCILob) {
|
||||
$binaryData->free();
|
||||
}
|
||||
|
||||
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): string {
|
||||
return ':' . ($count++);
|
||||
}, $sql);
|
||||
}
|
||||
}
|
||||
@@ -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): false|string => 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;
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,631 @@
|
||||
<?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 CodeIgniter\Exceptions\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 ? '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): string => $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): string|RawSql {
|
||||
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): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $index . ' ' . $key,
|
||||
$keys,
|
||||
$value,
|
||||
)),
|
||||
$values,
|
||||
),
|
||||
) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns cast expression.
|
||||
*
|
||||
* @TODO move this to BaseBuilder in 4.5.0
|
||||
*/
|
||||
private function cast(string $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): string => trim($columnName, '"'), $keys);
|
||||
|
||||
$constraints = $this->QBOptions['constraints'] ?? [];
|
||||
|
||||
if (empty($constraints)) {
|
||||
$allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames): bool {
|
||||
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
|
||||
|
||||
return ($index->type === 'UNIQUE' || $index->type === 'PRIMARY') && $hasAllFields;
|
||||
});
|
||||
|
||||
foreach ($allIndexes 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
|
||||
}
|
||||
|
||||
// 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): string => $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): RawSql|string {
|
||||
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): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $index . ' ' . $key,
|
||||
$keys,
|
||||
$value,
|
||||
)),
|
||||
$values,
|
||||
),
|
||||
) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,604 @@
|
||||
<?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 CodeIgniter\Database\TableName;
|
||||
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 ? pg_pconnect($this->DSN) : pg_connect($this->DSN);
|
||||
|
||||
if ($this->connID !== false) {
|
||||
if (
|
||||
$persistent
|
||||
&& 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 ($this->connID === false || pg_ping($this->connID) === false) {
|
||||
$this->close();
|
||||
$this->initialize();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
if ($this->resultID === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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 && $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.
|
||||
*
|
||||
* @param string|TableName $table
|
||||
*/
|
||||
protected function _listColumns($table = ''): string
|
||||
{
|
||||
if ($table instanceof TableName) {
|
||||
$tableName = $this->escape($table->getActualTableName());
|
||||
} else {
|
||||
$tableName = $this->escape($this->DBPrefix . strtolower($table));
|
||||
}
|
||||
|
||||
return 'SELECT "column_name"
|
||||
FROM "information_schema"."columns"
|
||||
WHERE LOWER("table_name") = ' . $tableName
|
||||
. ' 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): string => 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
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
<?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 ? ' 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']) && str_contains(strtolower($attributes['TYPE']), 'int')) {
|
||||
$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;
|
||||
|
||||
case 'BLOB':
|
||||
$attributes['TYPE'] = 'BYTEA';
|
||||
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) {
|
||||
$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, '"') . "'";
|
||||
}
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
<?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\BasePreparedQuery;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
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.');
|
||||
}
|
||||
|
||||
foreach ($data as &$item) {
|
||||
if (is_string($item) && $this->isBinary($item)) {
|
||||
$item = pg_escape_bytea($this->db->connID, $item);
|
||||
}
|
||||
}
|
||||
|
||||
$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): string {
|
||||
$count++;
|
||||
|
||||
return "\${$count}";
|
||||
}, $sql);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
+63
@@ -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 CodeIgniter\Exceptions\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;
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
<?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
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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) >= 1) {
|
||||
$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(?![^(')]*'(?:(?:[^(')]*'){2})*[^(')]*$)/';
|
||||
|
||||
return preg_replace_callback($search, static fn ($matches): string => '<strong>' . str_replace(' ', ' ', $matches[0]) . '</strong>', $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return text representation of the query
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getQuery();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
<?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 $this
|
||||
*/
|
||||
public function setQuery(string $sql, $binds = null, bool $setEscape = true);
|
||||
|
||||
/**
|
||||
* Returns the final, processed query string after binding, etal
|
||||
* has been performed.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
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 $this
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
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 $this
|
||||
*/
|
||||
public function swapPrefix(string $orig, string $swap);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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);
|
||||
}
|
||||
@@ -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) >= 1) {
|
||||
$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) {
|
||||
$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): string => trim($columnName, '"'), $keys);
|
||||
|
||||
if (empty($constraints)) {
|
||||
$tableIndexes = $this->db->getIndexData($table);
|
||||
|
||||
$uniqueIndexes = array_filter($tableIndexes, static function ($index) use ($fieldNames): bool {
|
||||
$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): bool {
|
||||
$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): string => $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): string => $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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,590 @@
|
||||
<?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\TableName;
|
||||
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 && $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.
|
||||
*
|
||||
* @param string|TableName $table
|
||||
*/
|
||||
protected function _listColumns($table = ''): string
|
||||
{
|
||||
if ($table instanceof TableName) {
|
||||
$tableName = $this->escape(strtolower($table->getActualTableName()));
|
||||
} else {
|
||||
$tableName = $this->escape($this->DBPrefix . strtolower($table));
|
||||
}
|
||||
|
||||
return 'SELECT [COLUMN_NAME] '
|
||||
. ' FROM [INFORMATION_SCHEMA].[COLUMNS]'
|
||||
. ' WHERE [TABLE_NAME] = ' . $tableName
|
||||
. ' 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): string => 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]->CHARACTER_MAXIMUM_LENGTH === -1
|
||||
? 'max'
|
||||
: $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
|
||||
{
|
||||
if ($this->resultID === false) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
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) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
if (($info = sqlsrv_server_info($this->connID)) === []) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return isset($info['SQLServerVersion']) ? $this->dataCache['version'] = $info['SQLServerVersion'] : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,453 @@
|
||||
<?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): string => '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 ? '' : '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']) && str_contains(strtolower($attributes['TYPE']), 'int')) {
|
||||
$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): int => strlen($value),
|
||||
$attributes['CONSTRAINT'],
|
||||
),
|
||||
);
|
||||
|
||||
$attributes['TYPE'] = 'VARCHAR';
|
||||
$attributes['CONSTRAINT'] = $maxLength;
|
||||
break;
|
||||
|
||||
case 'TIMESTAMP':
|
||||
$attributes['TYPE'] = 'DATETIME';
|
||||
break;
|
||||
|
||||
case 'BOOLEAN':
|
||||
$attributes['TYPE'] = 'BIT';
|
||||
break;
|
||||
|
||||
case 'BLOB':
|
||||
$attributes['TYPE'] = 'VARBINARY';
|
||||
$attributes['CONSTRAINT'] ??= 'MAX';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Field attribute AUTO_INCREMENT
|
||||
*/
|
||||
protected function _attributeAutoIncrement(array &$attributes, array &$field)
|
||||
{
|
||||
if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true && str_contains(strtolower($field['type']), strtolower('INT'))) {
|
||||
$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, '"') . "'";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?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\BasePreparedQuery;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
|
||||
/**
|
||||
* 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, $options);
|
||||
|
||||
// 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.
|
||||
*
|
||||
* @param array<int, mixed> $options
|
||||
*/
|
||||
protected function parameterize(string $queryString, array $options): array
|
||||
{
|
||||
$numberOfVariables = substr_count($queryString, '?');
|
||||
|
||||
$params = [];
|
||||
|
||||
for ($c = 0; $c < $numberOfVariables; $c++) {
|
||||
$this->parameters[$c] = null;
|
||||
if (isset($options[$c])) {
|
||||
$params[] = [&$this->parameters[$c], SQLSRV_PARAM_IN, $options[$c]];
|
||||
} else {
|
||||
$params[] = &$this->parameters[$c];
|
||||
}
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -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 CodeIgniter\Exceptions\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): string => trim($columnName, '`'), $keys);
|
||||
|
||||
$allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames): bool {
|
||||
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
|
||||
|
||||
return ($index->type === 'PRIMARY' || $index->type === 'UNIQUE') && $hasAllFields;
|
||||
});
|
||||
|
||||
foreach ($allIndexes 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'] ?? '`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): string => $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): string => $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): string => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index): string => $index . ' ' . $key,
|
||||
$keys,
|
||||
$value,
|
||||
)),
|
||||
$values,
|
||||
),
|
||||
) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
}
|
||||
@@ -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\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\TableName;
|
||||
use CodeIgniter\Exceptions\InvalidArgumentException;
|
||||
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;
|
||||
|
||||
/**
|
||||
* The setting of the "synchronous" flag
|
||||
*
|
||||
* @var int<0, 3>|null flag
|
||||
*
|
||||
* @see https://www.sqlite.org/pragma.html#pragma_synchronous
|
||||
*/
|
||||
protected ?int $synchronous = null;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
|
||||
if ($this->foreignKeys) {
|
||||
$this->enableForeignKeyChecks();
|
||||
}
|
||||
|
||||
if (is_int($this->busyTimeout)) {
|
||||
$this->connID->busyTimeout($this->busyTimeout);
|
||||
}
|
||||
|
||||
if (is_int($this->synchronous)) {
|
||||
if (! in_array($this->synchronous, [0, 1, 2, 3], true)) {
|
||||
throw new InvalidArgumentException('Invalid synchronous value.');
|
||||
}
|
||||
$this->connID->exec('PRAGMA synchronous = ' . $this->synchronous);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = (! isset($this->password) || $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 ((string) $tableName !== '') {
|
||||
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 && $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.
|
||||
*
|
||||
* @param string|TableName $table
|
||||
*/
|
||||
protected function _listColumns($table = ''): string
|
||||
{
|
||||
if ($table instanceof TableName) {
|
||||
$tableName = $this->escapeIdentifier($table);
|
||||
} else {
|
||||
$tableName = $this->protectIdentifiers($table, true, null, false);
|
||||
}
|
||||
|
||||
return 'PRAGMA TABLE_INFO(' . $tableName . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|TableName $tableName
|
||||
*
|
||||
* @return false|list<string>
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function getFieldNames($tableName)
|
||||
{
|
||||
$table = ($tableName instanceof TableName) ? $tableName->getTableName() : $tableName;
|
||||
|
||||
// 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($tableName);
|
||||
|
||||
$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()) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<?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 list<string>|string $columnNames
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function dropColumn(string $table, $columnNames): bool
|
||||
{
|
||||
$columns = is_array($columnNames) ? $columnNames : array_map(trim(...), explode(',', $columnNames));
|
||||
$result = (new Table($this->db, $this))
|
||||
->fromTable($this->db->DBPrefix . $table)
|
||||
->dropColumn($columns)
|
||||
->run();
|
||||
|
||||
if (! $result && $this->db->DBDebug) {
|
||||
throw new DatabaseException(sprintf(
|
||||
'Failed to drop column%s "%s" on "%s" table.',
|
||||
count($columns) > 1 ? 's' : '',
|
||||
implode('", "', $columns),
|
||||
$table,
|
||||
));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 '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
|
||||
&& str_contains(strtolower($field['type']), 'int')
|
||||
) {
|
||||
$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 [];
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
<?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\BasePreparedQuery;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Exceptions\BadMethodCallException;
|
||||
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;
|
||||
} elseif (is_string($item) && $this->isBinary($item)) {
|
||||
$bindType = SQLITE3_BLOB;
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* 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): bool => $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): bool => 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): bool => 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): bool => 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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?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\Exceptions\InvalidArgumentException;
|
||||
use Config\Database;
|
||||
use Faker\Factory;
|
||||
use Faker\Generator;
|
||||
|
||||
/**
|
||||
* 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 instanceof Generator && class_exists(Factory::class)) {
|
||||
self::$faker = Factory::create();
|
||||
}
|
||||
|
||||
return self::$faker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the specified seeder and runs it.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @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()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Represents a table name in SQL.
|
||||
*
|
||||
* @interal
|
||||
*
|
||||
* @see \CodeIgniter\Database\TableNameTest
|
||||
*/
|
||||
class TableName
|
||||
{
|
||||
/**
|
||||
* @param string $actualTable Actual table name
|
||||
* @param string $logicalTable Logical table name (w/o DB prefix)
|
||||
* @param string $schema Schema name
|
||||
* @param string $database Database name
|
||||
* @param string $alias Alias name
|
||||
*/
|
||||
protected function __construct(
|
||||
private readonly string $actualTable,
|
||||
private readonly string $logicalTable = '',
|
||||
private readonly string $schema = '',
|
||||
private readonly string $database = '',
|
||||
private readonly string $alias = '',
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance.
|
||||
*
|
||||
* @param string $table Table name (w/o DB prefix)
|
||||
* @param string $alias Alias name
|
||||
*/
|
||||
public static function create(string $dbPrefix, string $table, string $alias = ''): self
|
||||
{
|
||||
return new self(
|
||||
$dbPrefix . $table,
|
||||
$table,
|
||||
'',
|
||||
'',
|
||||
$alias,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance from an actual table name.
|
||||
*
|
||||
* @param string $actualTable Actual table name with DB prefix
|
||||
* @param string $alias Alias name
|
||||
*/
|
||||
public static function fromActualName(string $dbPrefix, string $actualTable, string $alias = ''): self
|
||||
{
|
||||
$prefix = $dbPrefix;
|
||||
$logicalTable = '';
|
||||
|
||||
if (str_starts_with($actualTable, $prefix)) {
|
||||
$logicalTable = substr($actualTable, strlen($prefix));
|
||||
}
|
||||
|
||||
return new self(
|
||||
$actualTable,
|
||||
$logicalTable,
|
||||
'',
|
||||
$alias,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance from full name.
|
||||
*
|
||||
* @param string $table Table name (w/o DB prefix)
|
||||
* @param string $schema Schema name
|
||||
* @param string $database Database name
|
||||
* @param string $alias Alias name
|
||||
*/
|
||||
public static function fromFullName(
|
||||
string $dbPrefix,
|
||||
string $table,
|
||||
string $schema = '',
|
||||
string $database = '',
|
||||
string $alias = '',
|
||||
): self {
|
||||
return new self(
|
||||
$dbPrefix . $table,
|
||||
$table,
|
||||
$schema,
|
||||
$database,
|
||||
$alias,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the single segment table name w/o DB prefix.
|
||||
*/
|
||||
public function getTableName(): string
|
||||
{
|
||||
return $this->logicalTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actual single segment table name w/z DB prefix.
|
||||
*/
|
||||
public function getActualTableName(): string
|
||||
{
|
||||
return $this->actualTable;
|
||||
}
|
||||
|
||||
public function getAlias(): string
|
||||
{
|
||||
return $this->alias;
|
||||
}
|
||||
|
||||
public function getSchema(): string
|
||||
{
|
||||
return $this->schema;
|
||||
}
|
||||
|
||||
public function getDatabase(): string
|
||||
{
|
||||
return $this->database;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user