Initial project files: BESCMS full source
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,265 @@
|
||||
<?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 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<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<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
|
||||
$this->db->handleTransStatus();
|
||||
|
||||
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,537 @@
|
||||
<?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 TConnection
|
||||
*/
|
||||
public $connID;
|
||||
|
||||
/**
|
||||
* Result ID
|
||||
*
|
||||
* @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 TConnection $connID
|
||||
* @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.
|
||||
*
|
||||
* @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 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 'array'|'object'|class-string<T> $type The type of result object. 'array', 'object' or class name.
|
||||
*
|
||||
* @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.
|
||||
* @param class-string<T> $className
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @param class-string $className
|
||||
*
|
||||
* @return false|object
|
||||
*/
|
||||
abstract protected function fetchObject(string $className = stdClass::class);
|
||||
}
|
||||
@@ -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,162 @@
|
||||
<?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|TConnection
|
||||
*/
|
||||
public function connect(bool $persistent = false);
|
||||
|
||||
/**
|
||||
* Create a persistent database connection.
|
||||
*
|
||||
* @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|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<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|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 ($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);
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,881 @@
|
||||
<?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'));
|
||||
//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<string, string>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,265 @@
|
||||
<?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 ($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,62 @@
|
||||
<?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<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,183 @@
|
||||
<?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 'array'|'object'|class-string<T> $type The type of result object. 'array', 'object' or class name.
|
||||
*
|
||||
* @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.
|
||||
* @param class-string<T> $className
|
||||
*
|
||||
* @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,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<string, string>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,337 @@
|
||||
<?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));
|
||||
$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 ($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 [];
|
||||
}
|
||||
}
|
||||
@@ -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,517 @@
|
||||
<?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));
|
||||
|
||||
$indexData = $this->db->getIndexData($table);
|
||||
$this->keys = array_merge($this->keys, $this->formatKeys(is_array($indexData) ? $indexData : []));
|
||||
|
||||
// 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']);
|
||||
}
|
||||
|
||||
$fkData = $this->db->getForeignKeyData($table);
|
||||
$this->foreignKeys = is_array($fkData) ? $fkData : [];
|
||||
|
||||
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
|
||||
{
|
||||
// 重建表前确保等待锁,避免立刻 database is locked
|
||||
if (method_exists($this->db, 'connID') || isset($this->db->connID)) {
|
||||
$conn = $this->db->connID ?? null;
|
||||
if ($conn instanceof \SQLite3) {
|
||||
$conn->busyTimeout(10000);
|
||||
@$conn->exec('PRAGMA busy_timeout = 10000');
|
||||
}
|
||||
}
|
||||
|
||||
$this->db->query('PRAGMA foreign_keys = OFF');
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
$success = false;
|
||||
|
||||
try {
|
||||
$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();
|
||||
} catch (\Throwable $e) {
|
||||
// 必须回滚,否则同请求后续操作会一直 database is locked
|
||||
$this->db->transRollback();
|
||||
// 尽量恢复外键后再抛出
|
||||
try {
|
||||
$this->db->query('PRAGMA foreign_keys = ON');
|
||||
} catch (\Throwable $ignore) {
|
||||
}
|
||||
$this->db->resetDataCache();
|
||||
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$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 ($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