Initial
This commit is contained in:
2758
system/Database/BaseBuilder.php
Normal file
2758
system/Database/BaseBuilder.php
Normal file
File diff suppressed because it is too large
Load Diff
1588
system/Database/BaseConnection.php
Normal file
1588
system/Database/BaseConnection.php
Normal file
File diff suppressed because it is too large
Load Diff
189
system/Database/BasePreparedQuery.php
Normal file
189
system/Database/BasePreparedQuery.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use BadMethodCallException;
|
||||
use CodeIgniter\Events\Events;
|
||||
|
||||
/**
|
||||
* Base prepared query
|
||||
*/
|
||||
abstract class BasePreparedQuery implements PreparedQueryInterface
|
||||
{
|
||||
/**
|
||||
* The prepared statement itself.
|
||||
*
|
||||
* @var object|resource
|
||||
*/
|
||||
protected $statement;
|
||||
|
||||
/**
|
||||
* The error code, if any.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $errorCode;
|
||||
|
||||
/**
|
||||
* The error message, if any.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $errorString;
|
||||
|
||||
/**
|
||||
* Holds the prepared query object
|
||||
* that is cloned during execute.
|
||||
*
|
||||
* @var Query
|
||||
*/
|
||||
protected $query;
|
||||
|
||||
/**
|
||||
* A reference to the db connection to use.
|
||||
*
|
||||
* @var BaseConnection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
public function __construct(BaseConnection $db)
|
||||
{
|
||||
$this->db = &$db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* NOTE: This version is based on SQL code. Child classes should
|
||||
* override this method.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function prepare(string $sql, array $options = [], string $queryClass = 'CodeIgniter\\Database\\Query')
|
||||
{
|
||||
// We only supports positional placeholders (?)
|
||||
// in order to work with the execute method below, so we
|
||||
// need to replace our named placeholders (:name)
|
||||
$sql = preg_replace('/:[^\s,)]+/', '?', $sql);
|
||||
|
||||
/** @var Query $query */
|
||||
$query = new $queryClass($this->db);
|
||||
|
||||
$query->setQuery($sql);
|
||||
|
||||
if (! empty($this->db->swapPre) && ! empty($this->db->DBPrefix)) {
|
||||
$query->swapPrefix($this->db->DBPrefix, $this->db->swapPre);
|
||||
}
|
||||
|
||||
$this->query = $query;
|
||||
|
||||
return $this->_prepare($query->getOriginalQuery(), $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* The database-dependent portion of the prepare statement.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function _prepare(string $sql, array $options = []);
|
||||
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*
|
||||
* @return ResultInterface
|
||||
*/
|
||||
public function execute(...$data)
|
||||
{
|
||||
// Execute the Query.
|
||||
$startTime = microtime(true);
|
||||
|
||||
$this->_execute($data);
|
||||
|
||||
// Update our query object
|
||||
$query = clone $this->query;
|
||||
$query->setBinds($data);
|
||||
|
||||
$query->setDuration($startTime);
|
||||
|
||||
// Let others do something with this query
|
||||
Events::trigger('DBQuery', $query);
|
||||
|
||||
// Return a result object
|
||||
$resultClass = str_replace('PreparedQuery', 'Result', static::class);
|
||||
|
||||
$resultID = $this->_getResult();
|
||||
|
||||
return new $resultClass($this->db->connID, $resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* The database dependant version of the execute method.
|
||||
*/
|
||||
abstract public function _execute(array $data): bool;
|
||||
|
||||
/**
|
||||
* Returns the result object for the prepared query.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function _getResult();
|
||||
|
||||
/**
|
||||
* Explicitly closes the statement.
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
if (! is_object($this->statement) || ! method_exists($this->statement, 'close')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->statement->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the SQL that has been prepared.
|
||||
*/
|
||||
public function getQueryString(): string
|
||||
{
|
||||
if (! $this->query instanceof QueryInterface) {
|
||||
throw new BadMethodCallException('Cannot call getQueryString on a prepared query until after the query has been prepared.');
|
||||
}
|
||||
|
||||
return $this->query->getQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to determine if any error exists.
|
||||
*/
|
||||
public function hasError(): bool
|
||||
{
|
||||
return ! empty($this->errorString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code created while executing this statement.
|
||||
*/
|
||||
public function getErrorCode(): int
|
||||
{
|
||||
return $this->errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error message created while executing this statement.
|
||||
*/
|
||||
public function getErrorMessage(): string
|
||||
{
|
||||
return $this->errorString;
|
||||
}
|
||||
}
|
||||
512
system/Database/BaseResult.php
Normal file
512
system/Database/BaseResult.php
Normal file
@@ -0,0 +1,512 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\Entity\Entity;
|
||||
|
||||
/**
|
||||
* Class BaseResult
|
||||
*/
|
||||
abstract class BaseResult implements ResultInterface
|
||||
{
|
||||
/**
|
||||
* Connection ID
|
||||
*
|
||||
* @var object|resource
|
||||
*/
|
||||
public $connID;
|
||||
|
||||
/**
|
||||
* Result ID
|
||||
*
|
||||
* @var false|object|resource
|
||||
*/
|
||||
public $resultID;
|
||||
|
||||
/**
|
||||
* Result Array
|
||||
*
|
||||
* @var array[]
|
||||
*/
|
||||
public $resultArray = [];
|
||||
|
||||
/**
|
||||
* Result Object
|
||||
*
|
||||
* @var object[]
|
||||
*/
|
||||
public $resultObject = [];
|
||||
|
||||
/**
|
||||
* Custom Result Object
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $customResultObject = [];
|
||||
|
||||
/**
|
||||
* Current Row index
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $currentRow = 0;
|
||||
|
||||
/**
|
||||
* The number of records in the query result
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
protected $numRows;
|
||||
|
||||
/**
|
||||
* Row data
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
public $rowData;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param object|resource $connID
|
||||
* @param object|resource $resultID
|
||||
*/
|
||||
public function __construct(&$connID, &$resultID)
|
||||
{
|
||||
$this->connID = $connID;
|
||||
$this->resultID = $resultID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the results of the query. Typically an array of
|
||||
* individual data rows, which can be either an 'array', an
|
||||
* 'object', or a custom class name.
|
||||
*
|
||||
* @param string $type The row type. Either 'array', 'object', or a class name to use
|
||||
*/
|
||||
public function getResult(string $type = 'object'): array
|
||||
{
|
||||
if ($type === 'array') {
|
||||
return $this->getResultArray();
|
||||
}
|
||||
|
||||
if ($type === 'object') {
|
||||
return $this->getResultObject();
|
||||
}
|
||||
|
||||
return $this->getCustomResultObject($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the results as an array of custom objects.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCustomResultObject(string $className)
|
||||
{
|
||||
if (isset($this->customResultObject[$className])) {
|
||||
return $this->customResultObject[$className];
|
||||
}
|
||||
|
||||
if (is_bool($this->resultID) || ! $this->resultID) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Don't fetch the result set again if we already have it
|
||||
$_data = null;
|
||||
if (($c = count($this->resultArray)) > 0) {
|
||||
$_data = 'resultArray';
|
||||
} elseif (($c = count($this->resultObject)) > 0) {
|
||||
$_data = 'resultObject';
|
||||
}
|
||||
|
||||
if ($_data !== null) {
|
||||
for ($i = 0; $i < $c; $i++) {
|
||||
$this->customResultObject[$className][$i] = new $className();
|
||||
|
||||
foreach ($this->{$_data}[$i] as $key => $value) {
|
||||
$this->customResultObject[$className][$i]->{$key} = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->customResultObject[$className];
|
||||
}
|
||||
|
||||
if ($this->rowData !== null) {
|
||||
$this->dataSeek();
|
||||
}
|
||||
$this->customResultObject[$className] = [];
|
||||
|
||||
while ($row = $this->fetchObject($className)) {
|
||||
if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) {
|
||||
$row->syncOriginal();
|
||||
}
|
||||
|
||||
$this->customResultObject[$className][] = $row;
|
||||
}
|
||||
|
||||
return $this->customResultObject[$className];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the results as an array of arrays.
|
||||
*
|
||||
* If no results, an empty array is returned.
|
||||
*/
|
||||
public function getResultArray(): array
|
||||
{
|
||||
if (! empty($this->resultArray)) {
|
||||
return $this->resultArray;
|
||||
}
|
||||
|
||||
// In the event that query caching is on, the result_id variable
|
||||
// will not be a valid resource so we'll simply return an empty
|
||||
// array.
|
||||
if (is_bool($this->resultID) || ! $this->resultID) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->resultObject) {
|
||||
foreach ($this->resultObject as $row) {
|
||||
$this->resultArray[] = (array) $row;
|
||||
}
|
||||
|
||||
return $this->resultArray;
|
||||
}
|
||||
|
||||
if ($this->rowData !== null) {
|
||||
$this->dataSeek();
|
||||
}
|
||||
|
||||
while ($row = $this->fetchAssoc()) {
|
||||
$this->resultArray[] = $row;
|
||||
}
|
||||
|
||||
return $this->resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the results as an array of objects.
|
||||
*
|
||||
* If no results, an empty array is returned.
|
||||
*/
|
||||
public function getResultObject(): array
|
||||
{
|
||||
if (! empty($this->resultObject)) {
|
||||
return $this->resultObject;
|
||||
}
|
||||
|
||||
// In the event that query caching is on, the result_id variable
|
||||
// will not be a valid resource so we'll simply return an empty
|
||||
// array.
|
||||
if (is_bool($this->resultID) || ! $this->resultID) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if ($this->resultArray) {
|
||||
foreach ($this->resultArray as $row) {
|
||||
$this->resultObject[] = (object) $row;
|
||||
}
|
||||
|
||||
return $this->resultObject;
|
||||
}
|
||||
|
||||
if ($this->rowData !== null) {
|
||||
$this->dataSeek();
|
||||
}
|
||||
|
||||
while ($row = $this->fetchObject()) {
|
||||
if (! is_subclass_of($row, Entity::class) && method_exists($row, 'syncOriginal')) {
|
||||
$row->syncOriginal();
|
||||
}
|
||||
|
||||
$this->resultObject[] = $row;
|
||||
}
|
||||
|
||||
return $this->resultObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper object to return a row as either an array, an object, or
|
||||
* a custom class.
|
||||
*
|
||||
* If row doesn't exist, returns null.
|
||||
*
|
||||
* @param mixed $n The index of the results to return
|
||||
* @param string $type The type of result object. 'array', 'object' or class name.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRow($n = 0, string $type = 'object')
|
||||
{
|
||||
if (! is_numeric($n)) {
|
||||
// We cache the row data for subsequent uses
|
||||
if (! is_array($this->rowData)) {
|
||||
$this->rowData = $this->getRowArray();
|
||||
}
|
||||
|
||||
// array_key_exists() instead of isset() to allow for NULL values
|
||||
if (empty($this->rowData) || ! array_key_exists($n, $this->rowData)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->rowData[$n];
|
||||
}
|
||||
|
||||
if ($type === 'object') {
|
||||
return $this->getRowObject($n);
|
||||
}
|
||||
|
||||
if ($type === 'array') {
|
||||
return $this->getRowArray($n);
|
||||
}
|
||||
|
||||
return $this->getCustomRowObject($n, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a row as a custom class instance.
|
||||
*
|
||||
* If row doesn't exists, returns null.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCustomRowObject(int $n, string $className)
|
||||
{
|
||||
if (! isset($this->customResultObject[$className])) {
|
||||
$this->getCustomResultObject($className);
|
||||
}
|
||||
|
||||
if (empty($this->customResultObject[$className])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($n !== $this->currentRow && isset($this->customResultObject[$className][$n])) {
|
||||
$this->currentRow = $n;
|
||||
}
|
||||
|
||||
return $this->customResultObject[$className][$this->currentRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single row from the results as an array.
|
||||
*
|
||||
* If row doesn't exist, returns null.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRowArray(int $n = 0)
|
||||
{
|
||||
$result = $this->getResultArray();
|
||||
if (empty($result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($n !== $this->currentRow && isset($result[$n])) {
|
||||
$this->currentRow = $n;
|
||||
}
|
||||
|
||||
return $result[$this->currentRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a single row from the results as an object.
|
||||
*
|
||||
* If row doesn't exist, returns null.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRowObject(int $n = 0)
|
||||
{
|
||||
$result = $this->getResultObject();
|
||||
if (empty($result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($n !== $this->customResultObject && isset($result[$n])) {
|
||||
$this->currentRow = $n;
|
||||
}
|
||||
|
||||
return $result[$this->currentRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns an item into a particular column slot.
|
||||
*
|
||||
* @param mixed $key
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function setRow($key, $value = null)
|
||||
{
|
||||
// We cache the row data for subsequent uses
|
||||
if (! is_array($this->rowData)) {
|
||||
$this->rowData = $this->getRowArray();
|
||||
}
|
||||
|
||||
if (is_array($key)) {
|
||||
foreach ($key as $k => $v) {
|
||||
$this->rowData[$k] = $v;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($key !== '' && $value !== null) {
|
||||
$this->rowData[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "first" row of the current results.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFirstRow(string $type = 'object')
|
||||
{
|
||||
$result = $this->getResult($type);
|
||||
|
||||
return (empty($result)) ? null : $result[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "last" row of the current results.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLastRow(string $type = 'object')
|
||||
{
|
||||
$result = $this->getResult($type);
|
||||
|
||||
return (empty($result)) ? null : $result[count($result) - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "next" row of the current results.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getNextRow(string $type = 'object')
|
||||
{
|
||||
$result = $this->getResult($type);
|
||||
if (empty($result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isset($result[$this->currentRow + 1]) ? $result[++$this->currentRow] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the "previous" row of the current results.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPreviousRow(string $type = 'object')
|
||||
{
|
||||
$result = $this->getResult($type);
|
||||
if (empty($result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isset($result[$this->currentRow - 1])) {
|
||||
$this->currentRow--;
|
||||
}
|
||||
|
||||
return $result[$this->currentRow];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unbuffered row and move the pointer to the next row.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUnbufferedRow(string $type = 'object')
|
||||
{
|
||||
if ($type === 'array') {
|
||||
return $this->fetchAssoc();
|
||||
}
|
||||
|
||||
if ($type === 'object') {
|
||||
return $this->fetchObject();
|
||||
}
|
||||
|
||||
return $this->fetchObject($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of rows in the result set; checks for previous count, falls
|
||||
* back on counting resultArray or resultObject, finally fetching resultArray
|
||||
* if nothing was previously fetched
|
||||
*/
|
||||
public function getNumRows(): int
|
||||
{
|
||||
if (is_int($this->numRows)) {
|
||||
return $this->numRows;
|
||||
}
|
||||
if ($this->resultArray !== []) {
|
||||
return $this->numRows = count($this->resultArray);
|
||||
}
|
||||
if ($this->resultObject !== []) {
|
||||
return $this->numRows = count($this->resultObject);
|
||||
}
|
||||
|
||||
return $this->numRows = count($this->getResultArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
abstract public function getFieldCount(): int;
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
abstract public function getFieldNames(): array;
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
abstract public function getFieldData(): array;
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*/
|
||||
abstract public function freeResult();
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function dataSeek(int $n = 0);
|
||||
|
||||
/**
|
||||
* Returns the result set as an array.
|
||||
*
|
||||
* Overridden by driver classes.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function fetchAssoc();
|
||||
|
||||
/**
|
||||
* Returns the result set as an object.
|
||||
*
|
||||
* Overridden by child classes.
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
abstract protected function fetchObject(string $className = 'stdClass');
|
||||
}
|
||||
321
system/Database/BaseUtils.php
Normal file
321
system/Database/BaseUtils.php
Normal file
@@ -0,0 +1,321 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* Class BaseUtils
|
||||
*/
|
||||
abstract class BaseUtils
|
||||
{
|
||||
/**
|
||||
* Database object
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* List databases statement
|
||||
*
|
||||
* @var bool|string
|
||||
*/
|
||||
protected $listDatabases = false;
|
||||
|
||||
/**
|
||||
* OPTIMIZE TABLE statement
|
||||
*
|
||||
* @var bool|string
|
||||
*/
|
||||
protected $optimizeTable = false;
|
||||
|
||||
/**
|
||||
* REPAIR TABLE statement
|
||||
*
|
||||
* @var bool|string
|
||||
*/
|
||||
protected $repairTable = false;
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*/
|
||||
public function __construct(ConnectionInterface &$db)
|
||||
{
|
||||
$this->db = &$db;
|
||||
}
|
||||
|
||||
/**
|
||||
* List databases
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public function listDatabases()
|
||||
{
|
||||
// Is there a cached result?
|
||||
if (isset($this->db->dataCache['db_names'])) {
|
||||
return $this->db->dataCache['db_names'];
|
||||
}
|
||||
|
||||
if ($this->listDatabases === false) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->db->dataCache['db_names'] = [];
|
||||
|
||||
$query = $this->db->query($this->listDatabases);
|
||||
if ($query === false) {
|
||||
return $this->db->dataCache['db_names'];
|
||||
}
|
||||
|
||||
for ($i = 0, $query = $query->getResultArray(), $c = count($query); $i < $c; $i++) {
|
||||
$this->db->dataCache['db_names'][] = current($query[$i]);
|
||||
}
|
||||
|
||||
return $this->db->dataCache['db_names'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a particular database exists
|
||||
*/
|
||||
public function databaseExists(string $databaseName): bool
|
||||
{
|
||||
return in_array($databaseName, $this->listDatabases(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize Table
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function optimizeTable(string $tableName)
|
||||
{
|
||||
if ($this->optimizeTable === false) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = $this->db->query(sprintf($this->optimizeTable, $this->db->escapeIdentifiers($tableName)));
|
||||
|
||||
return $query !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimize Database
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function optimizeDatabase()
|
||||
{
|
||||
if ($this->optimizeTable === false) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($this->db->listTables() as $tableName) {
|
||||
$res = $this->db->query(sprintf($this->optimizeTable, $this->db->escapeIdentifiers($tableName)));
|
||||
if (is_bool($res)) {
|
||||
return $res;
|
||||
}
|
||||
|
||||
// Build the result array...
|
||||
|
||||
$res = $res->getResultArray();
|
||||
|
||||
// Postgre & SQLite3 returns empty array
|
||||
if (empty($res)) {
|
||||
$key = $tableName;
|
||||
} else {
|
||||
$res = current($res);
|
||||
$key = str_replace($this->db->database . '.', '', current($res));
|
||||
$keys = array_keys($res);
|
||||
unset($res[$keys[0]]);
|
||||
}
|
||||
|
||||
$result[$key] = $res;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Repair Table
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function repairTable(string $tableName)
|
||||
{
|
||||
if ($this->repairTable === false) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = $this->db->query(sprintf($this->repairTable, $this->db->escapeIdentifiers($tableName)));
|
||||
if (is_bool($query)) {
|
||||
return $query;
|
||||
}
|
||||
|
||||
$query = $query->getResultArray();
|
||||
|
||||
return current($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSV from a query result object
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getCSVFromResult(ResultInterface $query, string $delim = ',', string $newline = "\n", string $enclosure = '"')
|
||||
{
|
||||
$out = '';
|
||||
|
||||
foreach ($query->getFieldNames() as $name) {
|
||||
$out .= $enclosure . str_replace($enclosure, $enclosure . $enclosure, $name) . $enclosure . $delim;
|
||||
}
|
||||
|
||||
$out = substr($out, 0, -strlen($delim)) . $newline;
|
||||
|
||||
// Next blast through the result array and build out the rows
|
||||
while ($row = $query->getUnbufferedRow('array')) {
|
||||
$line = [];
|
||||
|
||||
foreach ($row as $item) {
|
||||
$line[] = $enclosure . str_replace($enclosure, $enclosure . $enclosure, $item ?? '') . $enclosure;
|
||||
}
|
||||
|
||||
$out .= implode($delim, $line) . $newline;
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate XML data from a query result object
|
||||
*/
|
||||
public function getXMLFromResult(ResultInterface $query, array $params = []): string
|
||||
{
|
||||
foreach (['root' => 'root', 'element' => 'element', 'newline' => "\n", 'tab' => "\t"] as $key => $val) {
|
||||
if (! isset($params[$key])) {
|
||||
$params[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
$root = $params['root'];
|
||||
$newline = $params['newline'];
|
||||
$tab = $params['tab'];
|
||||
$element = $params['element'];
|
||||
|
||||
helper('xml');
|
||||
$xml = '<' . $root . '>' . $newline;
|
||||
|
||||
while ($row = $query->getUnbufferedRow()) {
|
||||
$xml .= $tab . '<' . $element . '>' . $newline;
|
||||
|
||||
foreach ($row as $key => $val) {
|
||||
$val = (! empty($val)) ? xml_convert($val) : '';
|
||||
|
||||
$xml .= $tab . $tab . '<' . $key . '>' . $val . '</' . $key . '>' . $newline;
|
||||
}
|
||||
|
||||
$xml .= $tab . '</' . $element . '>' . $newline;
|
||||
}
|
||||
|
||||
return $xml . '</' . $root . '>' . $newline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database Backup
|
||||
*
|
||||
* @param array|string $params
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function backup($params = [])
|
||||
{
|
||||
if (is_string($params)) {
|
||||
$params = ['tables' => $params];
|
||||
}
|
||||
|
||||
$prefs = [
|
||||
'tables' => [],
|
||||
'ignore' => [],
|
||||
'filename' => '',
|
||||
'format' => 'gzip', // gzip, txt
|
||||
'add_drop' => true,
|
||||
'add_insert' => true,
|
||||
'newline' => "\n",
|
||||
'foreign_key_checks' => true,
|
||||
];
|
||||
|
||||
if (! empty($params)) {
|
||||
foreach (array_keys($prefs) as $key) {
|
||||
if (isset($params[$key])) {
|
||||
$prefs[$key] = $params[$key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($prefs['tables'])) {
|
||||
$prefs['tables'] = $this->db->listTables();
|
||||
}
|
||||
|
||||
if (! in_array($prefs['format'], ['gzip', 'txt'], true)) {
|
||||
$prefs['format'] = 'txt';
|
||||
}
|
||||
|
||||
if ($prefs['format'] === 'gzip' && ! function_exists('gzencode')) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('The file compression format you chose is not supported by your server.');
|
||||
}
|
||||
|
||||
$prefs['format'] = 'txt';
|
||||
}
|
||||
|
||||
if ($prefs['format'] === 'txt') {
|
||||
return $this->_backup($prefs);
|
||||
}
|
||||
|
||||
return gzencode($this->_backup($prefs));
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform dependent version of the backup function.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
abstract public function _backup(?array $prefs = null);
|
||||
}
|
||||
145
system/Database/Config.php
Normal file
145
system/Database/Config.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class Config
|
||||
*/
|
||||
class Config extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* Cache for instance of any connections that
|
||||
* have been requested as a "shared" instance.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $instances = [];
|
||||
|
||||
/**
|
||||
* The main instance used to manage all of
|
||||
* our open database connections.
|
||||
*
|
||||
* @var Database|null
|
||||
*/
|
||||
protected static $factory;
|
||||
|
||||
/**
|
||||
* Creates the default
|
||||
*
|
||||
* @param array|string $group The name of the connection group to use, or an array of configuration settings.
|
||||
* @param bool $getShared Whether to return a shared instance of the connection.
|
||||
*
|
||||
* @return BaseConnection
|
||||
*/
|
||||
public static function connect($group = null, bool $getShared = true)
|
||||
{
|
||||
// If a DB connection is passed in, just pass it back
|
||||
if ($group instanceof BaseConnection) {
|
||||
return $group;
|
||||
}
|
||||
|
||||
if (is_array($group)) {
|
||||
$config = $group;
|
||||
$group = 'custom-' . md5(json_encode($config));
|
||||
}
|
||||
|
||||
$config = $config ?? config('Database');
|
||||
|
||||
if (empty($group)) {
|
||||
$group = ENVIRONMENT === 'testing' ? 'tests' : $config->defaultGroup;
|
||||
}
|
||||
|
||||
if (is_string($group) && ! isset($config->{$group}) && strpos($group, 'custom-') !== 0) {
|
||||
throw new InvalidArgumentException($group . ' is not a valid database connection group.');
|
||||
}
|
||||
|
||||
if ($getShared && isset(static::$instances[$group])) {
|
||||
return static::$instances[$group];
|
||||
}
|
||||
|
||||
static::ensureFactory();
|
||||
|
||||
if (isset($config->{$group})) {
|
||||
$config = $config->{$group};
|
||||
}
|
||||
|
||||
$connection = static::$factory->load($config, $group);
|
||||
|
||||
static::$instances[$group] = &$connection;
|
||||
|
||||
return $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all db connections currently made.
|
||||
*/
|
||||
public static function getConnections(): array
|
||||
{
|
||||
return static::$instances;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads and returns an instance of the Forge for the specified
|
||||
* database group, and loads the group if it hasn't been loaded yet.
|
||||
*
|
||||
* @param array|ConnectionInterface|string|null $group
|
||||
*
|
||||
* @return Forge
|
||||
*/
|
||||
public static function forge($group = null)
|
||||
{
|
||||
$db = static::connect($group);
|
||||
|
||||
return static::$factory->loadForge($db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the Database Utilities class.
|
||||
*
|
||||
* @param array|string|null $group
|
||||
*
|
||||
* @return BaseUtils
|
||||
*/
|
||||
public static function utils($group = null)
|
||||
{
|
||||
$db = static::connect($group);
|
||||
|
||||
return static::$factory->loadUtils($db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new instance of the Database Seeder.
|
||||
*
|
||||
* @return Seeder
|
||||
*/
|
||||
public static function seeder(?string $group = null)
|
||||
{
|
||||
$config = config('Database');
|
||||
|
||||
return new Seeder($config, static::connect($group));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the database Connection Manager/Factory is loaded and ready to use.
|
||||
*/
|
||||
protected static function ensureFactory()
|
||||
{
|
||||
if (static::$factory instanceof Database) {
|
||||
return;
|
||||
}
|
||||
|
||||
static::$factory = new Database();
|
||||
}
|
||||
}
|
||||
156
system/Database/ConnectionInterface.php
Normal file
156
system/Database/ConnectionInterface.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
/**
|
||||
* Interface ConnectionInterface
|
||||
*/
|
||||
interface ConnectionInterface
|
||||
{
|
||||
/**
|
||||
* Initializes the database connection/settings.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function initialize();
|
||||
|
||||
/**
|
||||
* Connect to the database.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function connect(bool $persistent = false);
|
||||
|
||||
/**
|
||||
* Create a persistent database connection.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function persistentConnect();
|
||||
|
||||
/**
|
||||
* Keep or establish the connection if no queries have been sent for
|
||||
* a length of time exceeding the server's idle timeout.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function reconnect();
|
||||
|
||||
/**
|
||||
* Returns the actual connection object. If both a 'read' and 'write'
|
||||
* connection has been specified, you can pass either term in to
|
||||
* get that connection. If you pass either alias in and only a single
|
||||
* connection is present, it must return the sole connection.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getConnection(?string $alias = null);
|
||||
|
||||
/**
|
||||
* Select a specific database table to use.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function setDatabase(string $databaseName);
|
||||
|
||||
/**
|
||||
* Returns the name of the current database being used.
|
||||
*/
|
||||
public function getDatabase(): string;
|
||||
|
||||
/**
|
||||
* Returns the last error encountered by this connection.
|
||||
* Must return this format: ['code' => string|int, 'message' => string]
|
||||
* intval(code) === 0 means "no error".
|
||||
*
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
public function error(): array;
|
||||
|
||||
/**
|
||||
* The name of the platform in use (MySQLi, mssql, etc)
|
||||
*/
|
||||
public function getPlatform(): string;
|
||||
|
||||
/**
|
||||
* Returns a string containing the version of the database being used.
|
||||
*/
|
||||
public function getVersion(): string;
|
||||
|
||||
/**
|
||||
* Orchestrates a query against the database. Queries must use
|
||||
* Database\Statement objects to store the query and build it.
|
||||
* This method works with the cache.
|
||||
*
|
||||
* Should automatically handle different connections for read/write
|
||||
* queries if needed.
|
||||
*
|
||||
* @param mixed ...$binds
|
||||
*
|
||||
* @return BaseResult|bool|Query
|
||||
*/
|
||||
public function query(string $sql, $binds = null);
|
||||
|
||||
/**
|
||||
* Performs a basic query against the database. No binding or caching
|
||||
* is performed, nor are transactions handled. Simply takes a raw
|
||||
* query string and returns the database-specific result id.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function simpleQuery(string $sql);
|
||||
|
||||
/**
|
||||
* Returns an instance of the query builder for this connection.
|
||||
*
|
||||
* @param array|string $tableName Table name.
|
||||
*
|
||||
* @return BaseBuilder Builder.
|
||||
*/
|
||||
public function table($tableName);
|
||||
|
||||
/**
|
||||
* Returns the last query's statement object.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLastQuery();
|
||||
|
||||
/**
|
||||
* "Smart" Escaping
|
||||
*
|
||||
* Escapes data based on type.
|
||||
* Sets boolean and null types.
|
||||
*
|
||||
* @param mixed $str
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function escape($str);
|
||||
|
||||
/**
|
||||
* Allows for custom calls to the database engine that are not
|
||||
* supported through our database layer.
|
||||
*
|
||||
* @param array ...$params
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function callFunction(string $functionName, ...$params);
|
||||
|
||||
/**
|
||||
* Determines if the statement is a write-type query or not.
|
||||
*
|
||||
* @param string $sql
|
||||
*/
|
||||
public function isWriteType($sql): bool;
|
||||
}
|
||||
138
system/Database/Database.php
Normal file
138
system/Database/Database.php
Normal file
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Database Connection Factory
|
||||
*
|
||||
* Creates and returns an instance of the appropriate DatabaseConnection
|
||||
*/
|
||||
class Database
|
||||
{
|
||||
/**
|
||||
* Maintains an array of the instances of all connections that have
|
||||
* been created.
|
||||
*
|
||||
* Helps to keep track of all open connections for performance
|
||||
* monitoring, logging, etc.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $connections = [];
|
||||
|
||||
/**
|
||||
* Parses the connection binds and returns an instance of the driver
|
||||
* ready to go.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function load(array $params = [], string $alias = '')
|
||||
{
|
||||
if ($alias === '') {
|
||||
throw new InvalidArgumentException('You must supply the parameter: alias.');
|
||||
}
|
||||
|
||||
if (! empty($params['DSN']) && strpos($params['DSN'], '://') !== false) {
|
||||
$params = $this->parseDSN($params);
|
||||
}
|
||||
|
||||
if (empty($params['DBDriver'])) {
|
||||
throw new InvalidArgumentException('You have not selected a database type to connect to.');
|
||||
}
|
||||
|
||||
$this->connections[$alias] = $this->initDriver($params['DBDriver'], 'Connection', $params);
|
||||
|
||||
return $this->connections[$alias];
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Forge instance for the current database type.
|
||||
*/
|
||||
public function loadForge(ConnectionInterface $db): object
|
||||
{
|
||||
if (! $db->connID) {
|
||||
$db->initialize();
|
||||
}
|
||||
|
||||
return $this->initDriver($db->DBDriver, 'Forge', $db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Utils instance for the current database type.
|
||||
*/
|
||||
public function loadUtils(ConnectionInterface $db): object
|
||||
{
|
||||
if (! $db->connID) {
|
||||
$db->initialize();
|
||||
}
|
||||
|
||||
return $this->initDriver($db->DBDriver, 'Utils', $db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse universal DSN string
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function parseDSN(array $params): array
|
||||
{
|
||||
$dsn = parse_url($params['DSN']);
|
||||
|
||||
if (! $dsn) {
|
||||
throw new InvalidArgumentException('Your DSN connection string is invalid.');
|
||||
}
|
||||
|
||||
$dsnParams = [
|
||||
'DSN' => '',
|
||||
'DBDriver' => $dsn['scheme'],
|
||||
'hostname' => isset($dsn['host']) ? rawurldecode($dsn['host']) : '',
|
||||
'port' => isset($dsn['port']) ? rawurldecode((string) $dsn['port']) : '',
|
||||
'username' => isset($dsn['user']) ? rawurldecode($dsn['user']) : '',
|
||||
'password' => isset($dsn['pass']) ? rawurldecode($dsn['pass']) : '',
|
||||
'database' => isset($dsn['path']) ? rawurldecode(substr($dsn['path'], 1)) : '',
|
||||
];
|
||||
|
||||
if (! empty($dsn['query'])) {
|
||||
parse_str($dsn['query'], $extra);
|
||||
|
||||
foreach ($extra as $key => $val) {
|
||||
if (is_string($val) && in_array(strtolower($val), ['true', 'false', 'null'], true)) {
|
||||
$val = $val === 'null' ? null : filter_var($val, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
|
||||
$dsnParams[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
return array_merge($params, $dsnParams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize database driver.
|
||||
*
|
||||
* @param array|object $argument
|
||||
*/
|
||||
protected function initDriver(string $driver, string $class, $argument): object
|
||||
{
|
||||
$class = $driver . '\\' . $class;
|
||||
|
||||
if (strpos($driver, '\\') === false) {
|
||||
$class = "CodeIgniter\\Database\\{$class}";
|
||||
}
|
||||
|
||||
return new $class($argument);
|
||||
}
|
||||
}
|
||||
85
system/Database/Exceptions/DataException.php
Normal file
85
system/Database/Exceptions/DataException.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Exceptions;
|
||||
|
||||
use CodeIgniter\Exceptions\DebugTraceableTrait;
|
||||
use RuntimeException;
|
||||
|
||||
class DataException extends RuntimeException implements ExceptionInterface
|
||||
{
|
||||
use DebugTraceableTrait;
|
||||
|
||||
/**
|
||||
* Used by the Model's trigger() method when the callback cannot be found.
|
||||
*
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forInvalidMethodTriggered(string $method)
|
||||
{
|
||||
return new static(lang('Database.invalidEvent', [$method]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by Model's insert/update methods when there isn't
|
||||
* any data to actually work with.
|
||||
*
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forEmptyDataset(string $mode)
|
||||
{
|
||||
return new static(lang('Database.emptyDataset', [$mode]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by Model's insert/update methods when there is no
|
||||
* primary key defined and Model has option `useAutoIncrement`
|
||||
* set to false.
|
||||
*
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forEmptyPrimaryKey(string $mode)
|
||||
{
|
||||
return new static(lang('Database.emptyPrimaryKey', [$mode]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Thrown when an argument for one of the Model's methods
|
||||
* were empty or otherwise invalid, and they could not be
|
||||
* to work correctly for that method.
|
||||
*
|
||||
* @return DataException
|
||||
*/
|
||||
public static function forInvalidArgument(string $argument)
|
||||
{
|
||||
return new static(lang('Database.invalidArgument', [$argument]));
|
||||
}
|
||||
|
||||
public static function forInvalidAllowedFields(string $model)
|
||||
{
|
||||
return new static(lang('Database.invalidAllowedFields', [$model]));
|
||||
}
|
||||
|
||||
public static function forTableNotFound(string $table)
|
||||
{
|
||||
return new static(lang('Database.tableNotFound', [$table]));
|
||||
}
|
||||
|
||||
public static function forEmptyInputGiven(string $argument)
|
||||
{
|
||||
return new static(lang('Database.forEmptyInputGiven', [$argument]));
|
||||
}
|
||||
|
||||
public static function forFindColumnHaveMultipleColumns()
|
||||
{
|
||||
return new static(lang('Database.forFindColumnHaveMultipleColumns'));
|
||||
}
|
||||
}
|
||||
24
system/Database/Exceptions/DatabaseException.php
Normal file
24
system/Database/Exceptions/DatabaseException.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Exceptions;
|
||||
|
||||
use Error;
|
||||
|
||||
class DatabaseException extends Error implements ExceptionInterface
|
||||
{
|
||||
/**
|
||||
* Exit status code
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $code = 8;
|
||||
}
|
||||
22
system/Database/Exceptions/ExceptionInterface.php
Normal file
22
system/Database/Exceptions/ExceptionInterface.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Exceptions;
|
||||
|
||||
/**
|
||||
* Provides a domain-level interface for broad capture
|
||||
* of all database-related exceptions.
|
||||
*
|
||||
* catch (\CodeIgniter\Database\Exceptions\ExceptionInterface) { ... }
|
||||
*/
|
||||
interface ExceptionInterface extends \CodeIgniter\Exceptions\ExceptionInterface
|
||||
{
|
||||
}
|
||||
1099
system/Database/Forge.php
Normal file
1099
system/Database/Forge.php
Normal file
File diff suppressed because it is too large
Load Diff
73
system/Database/Migration.php
Normal file
73
system/Database/Migration.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use Config\Database;
|
||||
|
||||
/**
|
||||
* Class Migration
|
||||
*/
|
||||
abstract class Migration
|
||||
{
|
||||
/**
|
||||
* The name of the database group to use.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $DBGroup;
|
||||
|
||||
/**
|
||||
* Database Connection instance
|
||||
*
|
||||
* @var ConnectionInterface
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* Database Forge instance.
|
||||
*
|
||||
* @var Forge
|
||||
*/
|
||||
protected $forge;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param Forge $forge
|
||||
*/
|
||||
public function __construct(?Forge $forge = null)
|
||||
{
|
||||
$this->forge = $forge ?? Database::forge($this->DBGroup ?? config('Database')->defaultGroup);
|
||||
|
||||
$this->db = $this->forge->getConnection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the database group name this migration uses.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDBGroup(): ?string
|
||||
{
|
||||
return $this->DBGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a migration step.
|
||||
*/
|
||||
abstract public function up();
|
||||
|
||||
/**
|
||||
* Revert a migration step.
|
||||
*/
|
||||
abstract public function down();
|
||||
}
|
||||
867
system/Database/MigrationRunner.php
Normal file
867
system/Database/MigrationRunner.php
Normal file
@@ -0,0 +1,867 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\CLI\CLI;
|
||||
use CodeIgniter\Events\Events;
|
||||
use CodeIgniter\Exceptions\ConfigException;
|
||||
use Config\Database;
|
||||
use Config\Migrations as MigrationsConfig;
|
||||
use Config\Services;
|
||||
use RuntimeException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Class MigrationRunner
|
||||
*/
|
||||
class MigrationRunner
|
||||
{
|
||||
/**
|
||||
* Whether or not migrations are allowed to run.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $enabled = false;
|
||||
|
||||
/**
|
||||
* Name of table to store meta information
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table;
|
||||
|
||||
/**
|
||||
* The Namespace where migrations can be found.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $namespace;
|
||||
|
||||
/**
|
||||
* The database Group to migrate.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $group;
|
||||
|
||||
/**
|
||||
* The migration name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The pattern used to locate migration file versions.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $regex = '/^\d{4}[_-]?\d{2}[_-]?\d{2}[_-]?\d{6}_(\w+)$/';
|
||||
|
||||
/**
|
||||
* The main database connection. Used to store
|
||||
* migration information in.
|
||||
*
|
||||
* @var BaseConnection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* If true, will continue instead of throwing
|
||||
* exceptions.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $silent = false;
|
||||
|
||||
/**
|
||||
* used to return messages for CLI.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $cliMessages = [];
|
||||
|
||||
/**
|
||||
* Tracks whether we have already ensured
|
||||
* the table exists or not.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $tableChecked = false;
|
||||
|
||||
/**
|
||||
* The full path to locate migration files.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* The database Group filter.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $groupFilter;
|
||||
|
||||
/**
|
||||
* Used to skip current migration.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $groupSkip = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* When passing in $db, you may pass any of the following to connect:
|
||||
* - group name
|
||||
* - existing connection instance
|
||||
* - array of database configuration values
|
||||
*
|
||||
* @param array|ConnectionInterface|string|null $db
|
||||
*
|
||||
* @throws ConfigException
|
||||
*/
|
||||
public function __construct(MigrationsConfig $config, $db = null)
|
||||
{
|
||||
$this->enabled = $config->enabled ?? false;
|
||||
$this->table = $config->table ?? 'migrations';
|
||||
|
||||
// Default name space is the app namespace
|
||||
$this->namespace = APP_NAMESPACE;
|
||||
|
||||
// get default database group
|
||||
$config = config('Database');
|
||||
$this->group = $config->defaultGroup;
|
||||
unset($config);
|
||||
|
||||
// If no db connection passed in, use
|
||||
// default database group.
|
||||
$this->db = db_connect($db);
|
||||
}
|
||||
|
||||
/**
|
||||
* Locate and run all new migrations
|
||||
*
|
||||
* @throws ConfigException
|
||||
* @throws RuntimeException
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function latest(?string $group = null)
|
||||
{
|
||||
if (! $this->enabled) {
|
||||
throw ConfigException::forDisabledMigrations();
|
||||
}
|
||||
|
||||
$this->ensureTable();
|
||||
|
||||
if ($group !== null) {
|
||||
$this->groupFilter = $group;
|
||||
$this->setGroup($group);
|
||||
}
|
||||
|
||||
$migrations = $this->findMigrations();
|
||||
|
||||
if (empty($migrations)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($this->getHistory((string) $group) as $history) {
|
||||
unset($migrations[$this->getObjectUid($history)]);
|
||||
}
|
||||
|
||||
$batch = $this->getLastBatch() + 1;
|
||||
|
||||
foreach ($migrations as $migration) {
|
||||
if ($this->migrate('up', $migration)) {
|
||||
if ($this->groupSkip === true) {
|
||||
$this->groupSkip = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->addHistory($migration, $batch);
|
||||
} else {
|
||||
$this->regress(-1);
|
||||
|
||||
$message = lang('Migrations.generalFault');
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
$data = get_object_vars($this);
|
||||
$data['method'] = 'latest';
|
||||
Events::trigger('migrate', $data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate down to a previous batch
|
||||
*
|
||||
* Calls each migration step required to get to the provided batch
|
||||
*
|
||||
* @param int $targetBatch Target batch number, or negative for a relative batch, 0 for all
|
||||
*
|
||||
* @throws ConfigException
|
||||
* @throws RuntimeException
|
||||
*
|
||||
* @return mixed Current batch number on success, FALSE on failure or no migrations are found
|
||||
*/
|
||||
public function regress(int $targetBatch = 0, ?string $group = null)
|
||||
{
|
||||
if (! $this->enabled) {
|
||||
throw ConfigException::forDisabledMigrations();
|
||||
}
|
||||
|
||||
// Set database group if not null
|
||||
if ($group !== null) {
|
||||
$this->setGroup($group);
|
||||
}
|
||||
|
||||
$this->ensureTable();
|
||||
|
||||
$batches = $this->getBatches();
|
||||
|
||||
if ($targetBatch < 0) {
|
||||
$targetBatch = $batches[count($batches) - 1 + $targetBatch] ?? 0;
|
||||
}
|
||||
|
||||
if (empty($batches) && $targetBatch === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($targetBatch !== 0 && ! in_array($targetBatch, $batches, true)) {
|
||||
$message = lang('Migrations.batchNotFound') . $targetBatch;
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$tmpNamespace = $this->namespace;
|
||||
|
||||
$this->namespace = null;
|
||||
$allMigrations = $this->findMigrations();
|
||||
|
||||
$migrations = [];
|
||||
|
||||
while ($batch = array_pop($batches)) {
|
||||
if ($batch <= $targetBatch) {
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ($this->getBatchHistory($batch, 'desc') as $history) {
|
||||
$uid = $this->getObjectUid($history);
|
||||
|
||||
if (! isset($allMigrations[$uid])) {
|
||||
$message = lang('Migrations.gap') . ' ' . $history->version;
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$migration = $allMigrations[$uid];
|
||||
$migration->history = $history;
|
||||
$migrations[] = $migration;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($migrations as $migration) {
|
||||
if ($this->migrate('down', $migration)) {
|
||||
$this->removeHistory($migration->history);
|
||||
} else {
|
||||
$message = lang('Migrations.generalFault');
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
}
|
||||
|
||||
$data = get_object_vars($this);
|
||||
$data['method'] = 'regress';
|
||||
Events::trigger('migrate', $data);
|
||||
|
||||
$this->namespace = $tmpNamespace;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a single file regardless of order or batches.
|
||||
* Method "up" or "down" determined by presence in history.
|
||||
* NOTE: This is not recommended and provided mostly for testing.
|
||||
*
|
||||
* @param string $path Full path to a valid migration file
|
||||
* @param string $path Namespace of the target migration
|
||||
*/
|
||||
public function force(string $path, string $namespace, ?string $group = null)
|
||||
{
|
||||
if (! $this->enabled) {
|
||||
throw ConfigException::forDisabledMigrations();
|
||||
}
|
||||
|
||||
$this->ensureTable();
|
||||
|
||||
if ($group !== null) {
|
||||
$this->groupFilter = $group;
|
||||
$this->setGroup($group);
|
||||
}
|
||||
|
||||
$migration = $this->migrationFromFile($path, $namespace);
|
||||
if (empty($migration)) {
|
||||
$message = lang('Migrations.notFound');
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$method = 'up';
|
||||
$this->setNamespace($migration->namespace);
|
||||
|
||||
foreach ($this->getHistory($this->group) as $history) {
|
||||
if ($this->getObjectUid($history) === $migration->uid) {
|
||||
$method = 'down';
|
||||
$migration->history = $history;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($method === 'up') {
|
||||
$batch = $this->getLastBatch() + 1;
|
||||
|
||||
if ($this->migrate('up', $migration) && $this->groupSkip === false) {
|
||||
$this->addHistory($migration, $batch);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->groupSkip = false;
|
||||
} elseif ($this->migrate('down', $migration)) {
|
||||
$this->removeHistory($migration->history);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$message = lang('Migrations.generalFault');
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves list of available migration scripts
|
||||
*
|
||||
* @return array List of all located migrations by their UID
|
||||
*/
|
||||
public function findMigrations(): array
|
||||
{
|
||||
$namespaces = $this->namespace ? [$this->namespace] : array_keys(Services::autoloader()->getNamespace());
|
||||
$migrations = [];
|
||||
|
||||
foreach ($namespaces as $namespace) {
|
||||
foreach ($this->findNamespaceMigrations($namespace) as $migration) {
|
||||
$migrations[$migration->uid] = $migration;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort migrations ascending by their UID (version)
|
||||
ksort($migrations);
|
||||
|
||||
return $migrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of available migration scripts for one namespace
|
||||
*/
|
||||
public function findNamespaceMigrations(string $namespace): array
|
||||
{
|
||||
$migrations = [];
|
||||
$locator = Services::locator(true);
|
||||
|
||||
if (! empty($this->path)) {
|
||||
helper('filesystem');
|
||||
$dir = rtrim($this->path, DIRECTORY_SEPARATOR) . '/';
|
||||
$files = get_filenames($dir, true);
|
||||
} else {
|
||||
$files = $locator->listNamespaceFiles($namespace, '/Database/Migrations/');
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
$file = empty($this->path) ? $file : $this->path . str_replace($this->path, '', $file);
|
||||
|
||||
if ($migration = $this->migrationFromFile($file, $namespace)) {
|
||||
$migrations[] = $migration;
|
||||
}
|
||||
}
|
||||
|
||||
return $migrations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a migration object from a file path.
|
||||
*
|
||||
* @return false|object Returns the migration object, or false on failure
|
||||
*/
|
||||
protected function migrationFromFile(string $path, string $namespace)
|
||||
{
|
||||
if (substr($path, -4) !== '.php') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$name = basename($path, '.php');
|
||||
|
||||
if (! preg_match($this->regex, $name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$locator = Services::locator(true);
|
||||
|
||||
$migration = new stdClass();
|
||||
|
||||
$migration->version = $this->getMigrationNumber($name);
|
||||
$migration->name = $this->getMigrationName($name);
|
||||
$migration->path = $path;
|
||||
$migration->class = $locator->getClassname($path);
|
||||
$migration->namespace = $namespace;
|
||||
$migration->uid = $this->getObjectUid($migration);
|
||||
|
||||
return $migration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows other scripts to modify on the fly as needed.
|
||||
*
|
||||
* @return MigrationRunner
|
||||
*/
|
||||
public function setNamespace(?string $namespace)
|
||||
{
|
||||
$this->namespace = $namespace;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows other scripts to modify on the fly as needed.
|
||||
*
|
||||
* @return MigrationRunner
|
||||
*/
|
||||
public function setGroup(string $group)
|
||||
{
|
||||
$this->group = $group;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MigrationRunner
|
||||
*/
|
||||
public function setName(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* If $silent == true, then will not throw exceptions and will
|
||||
* attempt to continue gracefully.
|
||||
*
|
||||
* @return MigrationRunner
|
||||
*/
|
||||
public function setSilent(bool $silent)
|
||||
{
|
||||
$this->silent = $silent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the migration number from a filename
|
||||
*/
|
||||
protected function getMigrationNumber(string $migration): string
|
||||
{
|
||||
preg_match('/^\d{4}[_-]?\d{2}[_-]?\d{2}[_-]?\d{6}/', $migration, $matches);
|
||||
|
||||
return count($matches) ? $matches[0] : '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the migration class name from a filename
|
||||
*/
|
||||
protected function getMigrationName(string $migration): string
|
||||
{
|
||||
$parts = explode('_', $migration);
|
||||
array_shift($parts);
|
||||
|
||||
return implode('_', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the non-repeatable portions of a migration or history
|
||||
* to create a sortable unique key
|
||||
*
|
||||
* @param object $object migration or $history
|
||||
*/
|
||||
public function getObjectUid($object): string
|
||||
{
|
||||
return preg_replace('/[^0-9]/', '', $object->version) . $object->class;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves messages formatted for CLI output
|
||||
*/
|
||||
public function getCliMessages(): array
|
||||
{
|
||||
return $this->cliMessages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears any CLI messages.
|
||||
*
|
||||
* @return MigrationRunner
|
||||
*/
|
||||
public function clearCliMessages()
|
||||
{
|
||||
$this->cliMessages = [];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates the history table.
|
||||
*/
|
||||
public function clearHistory()
|
||||
{
|
||||
if ($this->db->tableExists($this->table)) {
|
||||
$this->db->table($this->table)->truncate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a history to the table.
|
||||
*
|
||||
* @param object $migration
|
||||
*/
|
||||
protected function addHistory($migration, int $batch)
|
||||
{
|
||||
$this->db->table($this->table)->insert([
|
||||
'version' => $migration->version,
|
||||
'class' => $migration->class,
|
||||
'group' => $this->group,
|
||||
'namespace' => $migration->namespace,
|
||||
'time' => time(),
|
||||
'batch' => $batch,
|
||||
]);
|
||||
|
||||
if (is_cli()) {
|
||||
$this->cliMessages[] = sprintf(
|
||||
"\t%s(%s) %s_%s",
|
||||
CLI::color(lang('Migrations.added'), 'yellow'),
|
||||
$migration->namespace,
|
||||
$migration->version,
|
||||
$migration->class
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a single history
|
||||
*
|
||||
* @param object $history
|
||||
*/
|
||||
protected function removeHistory($history)
|
||||
{
|
||||
$this->db->table($this->table)->where('id', $history->id)->delete();
|
||||
|
||||
if (is_cli()) {
|
||||
$this->cliMessages[] = sprintf(
|
||||
"\t%s(%s) %s_%s",
|
||||
CLI::color(lang('Migrations.removed'), 'yellow'),
|
||||
$history->namespace,
|
||||
$history->version,
|
||||
$history->class
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grabs the full migration history from the database for a group
|
||||
*/
|
||||
public function getHistory(string $group = 'default'): array
|
||||
{
|
||||
$this->ensureTable();
|
||||
|
||||
$builder = $this->db->table($this->table);
|
||||
|
||||
// If group was specified then use it
|
||||
if (! empty($group)) {
|
||||
$builder->where('group', $group);
|
||||
}
|
||||
|
||||
// If a namespace was specified then use it
|
||||
if ($this->namespace) {
|
||||
$builder->where('namespace', $this->namespace);
|
||||
}
|
||||
|
||||
$query = $builder->orderBy('id', 'ASC')->get();
|
||||
|
||||
return ! empty($query) ? $query->getResultObject() : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the migration history for a single batch.
|
||||
*
|
||||
* @param string $order
|
||||
*/
|
||||
public function getBatchHistory(int $batch, $order = 'asc'): array
|
||||
{
|
||||
$this->ensureTable();
|
||||
|
||||
$query = $this->db->table($this->table)
|
||||
->where('batch', $batch)
|
||||
->orderBy('id', $order)
|
||||
->get();
|
||||
|
||||
return ! empty($query) ? $query->getResultObject() : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all the batches from the database history in order
|
||||
*/
|
||||
public function getBatches(): array
|
||||
{
|
||||
$this->ensureTable();
|
||||
|
||||
$batches = $this->db->table($this->table)
|
||||
->select('batch')
|
||||
->distinct()
|
||||
->orderBy('batch', 'asc')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
return array_map('intval', array_column($batches, 'batch'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of the last batch in the database.
|
||||
*/
|
||||
public function getLastBatch(): int
|
||||
{
|
||||
$this->ensureTable();
|
||||
|
||||
$batch = $this->db->table($this->table)
|
||||
->selectMax('batch')
|
||||
->get()
|
||||
->getResultObject();
|
||||
|
||||
$batch = is_array($batch) && count($batch)
|
||||
? end($batch)->batch
|
||||
: 0;
|
||||
|
||||
return (int) $batch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version number of the first migration for a batch.
|
||||
* Mostly just for tests.
|
||||
*/
|
||||
public function getBatchStart(int $batch): string
|
||||
{
|
||||
if ($batch < 0) {
|
||||
$batches = $this->getBatches();
|
||||
$batch = $batches[count($batches) - 1] ?? 0;
|
||||
}
|
||||
|
||||
$migration = $this->db->table($this->table)
|
||||
->where('batch', $batch)
|
||||
->orderBy('id', 'asc')
|
||||
->limit(1)
|
||||
->get()
|
||||
->getResultObject();
|
||||
|
||||
return count($migration) ? $migration[0]->version : '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the version number of the last migration for a batch.
|
||||
* Mostly just for tests.
|
||||
*/
|
||||
public function getBatchEnd(int $batch): string
|
||||
{
|
||||
if ($batch < 0) {
|
||||
$batches = $this->getBatches();
|
||||
$batch = $batches[count($batches) - 1] ?? 0;
|
||||
}
|
||||
|
||||
$migration = $this->db->table($this->table)
|
||||
->where('batch', $batch)
|
||||
->orderBy('id', 'desc')
|
||||
->limit(1)
|
||||
->get()
|
||||
->getResultObject();
|
||||
|
||||
return count($migration) ? $migration[0]->version : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that we have created our migrations table
|
||||
* in the database.
|
||||
*/
|
||||
public function ensureTable()
|
||||
{
|
||||
if ($this->tableChecked || $this->db->tableExists($this->table)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$forge = Database::forge($this->db);
|
||||
|
||||
$forge->addField([
|
||||
'id' => [
|
||||
'type' => 'BIGINT',
|
||||
'constraint' => 20,
|
||||
'unsigned' => true,
|
||||
'auto_increment' => true,
|
||||
],
|
||||
'version' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => false,
|
||||
],
|
||||
'class' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => false,
|
||||
],
|
||||
'group' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => false,
|
||||
],
|
||||
'namespace' => [
|
||||
'type' => 'VARCHAR',
|
||||
'constraint' => 255,
|
||||
'null' => false,
|
||||
],
|
||||
'time' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'null' => false,
|
||||
],
|
||||
'batch' => [
|
||||
'type' => 'INT',
|
||||
'constraint' => 11,
|
||||
'unsigned' => true,
|
||||
'null' => false,
|
||||
],
|
||||
]);
|
||||
|
||||
$forge->addPrimaryKey('id');
|
||||
$forge->createTable($this->table, true);
|
||||
|
||||
$this->tableChecked = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the actual running of a migration.
|
||||
*
|
||||
* @param string $direction "up" or "down"
|
||||
* @param object $migration The migration to run
|
||||
*/
|
||||
protected function migrate($direction, $migration): bool
|
||||
{
|
||||
include_once $migration->path;
|
||||
|
||||
$class = $migration->class;
|
||||
$this->setName($migration->name);
|
||||
|
||||
// Validate the migration file structure
|
||||
if (! class_exists($class, false)) {
|
||||
$message = sprintf(lang('Migrations.classNotFound'), $class);
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$instance = new $class();
|
||||
$group = $instance->getDBGroup() ?? config('Database')->defaultGroup;
|
||||
|
||||
if (ENVIRONMENT !== 'testing' && $group === 'tests' && $this->groupFilter !== 'tests') {
|
||||
// @codeCoverageIgnoreStart
|
||||
$this->groupSkip = true;
|
||||
|
||||
return true;
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
if ($direction === 'up' && $this->groupFilter !== null && $this->groupFilter !== $group) {
|
||||
$this->groupSkip = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->setGroup($group);
|
||||
|
||||
if (! is_callable([$instance, $direction])) {
|
||||
$message = sprintf(lang('Migrations.missingMethod'), $direction);
|
||||
|
||||
if ($this->silent) {
|
||||
$this->cliMessages[] = "\t" . CLI::color($message, 'red');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
$instance->{$direction}();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
52
system/Database/ModelFactory.php
Normal file
52
system/Database/ModelFactory.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\Config\Factories;
|
||||
|
||||
/**
|
||||
* Returns new or shared Model instances
|
||||
*
|
||||
* @deprecated Use CodeIgniter\Config\Factories::models()
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ModelFactory
|
||||
{
|
||||
/**
|
||||
* Creates new Model instances or returns a shared instance
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get(string $name, bool $getShared = true, ?ConnectionInterface $connection = null)
|
||||
{
|
||||
return Factories::models($name, ['getShared' => $getShared], $connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for injecting mock instances while testing.
|
||||
*
|
||||
* @param object $instance
|
||||
*/
|
||||
public static function injectMock(string $name, $instance)
|
||||
{
|
||||
Factories::injectMock('models', $name, $instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the static arrays
|
||||
*/
|
||||
public static function reset()
|
||||
{
|
||||
Factories::reset('models');
|
||||
}
|
||||
}
|
||||
56
system/Database/MySQLi/Builder.php
Normal file
56
system/Database/MySQLi/Builder.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\MySQLi;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
|
||||
/**
|
||||
* Builder for MySQLi
|
||||
*/
|
||||
class Builder extends BaseBuilder
|
||||
{
|
||||
/**
|
||||
* Identifier escape character
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $escapeChar = '`';
|
||||
|
||||
/**
|
||||
* Specifies which sql statements
|
||||
* support the ignore option.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $supportedIgnoreStatements = [
|
||||
'update' => 'IGNORE',
|
||||
'insert' => 'IGNORE',
|
||||
'delete' => 'IGNORE',
|
||||
];
|
||||
|
||||
/**
|
||||
* FROM tables
|
||||
*
|
||||
* Groups tables in FROM clauses if needed, so there is no confusion
|
||||
* about operator precedence.
|
||||
*
|
||||
* Note: This is only used (and overridden) by MySQL.
|
||||
*/
|
||||
protected function _fromTables(): string
|
||||
{
|
||||
if (! empty($this->QBJoin) && count($this->QBFrom) > 1) {
|
||||
return '(' . implode(', ', $this->QBFrom) . ')';
|
||||
}
|
||||
|
||||
return implode(', ', $this->QBFrom);
|
||||
}
|
||||
}
|
||||
605
system/Database/MySQLi/Connection.php
Normal file
605
system/Database/MySQLi/Connection.php
Normal file
@@ -0,0 +1,605 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\MySQLi;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use LogicException;
|
||||
use MySQLi;
|
||||
use mysqli_sql_exception;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Connection for MySQLi
|
||||
*/
|
||||
class Connection extends BaseConnection
|
||||
{
|
||||
/**
|
||||
* Database driver
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $DBDriver = 'MySQLi';
|
||||
|
||||
/**
|
||||
* DELETE hack flag
|
||||
*
|
||||
* Whether to use the MySQL "delete hack" which allows the number
|
||||
* of affected rows to be shown. Uses a preg_replace when enabled,
|
||||
* adding a bit more processing to all queries.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteHack = true;
|
||||
|
||||
/**
|
||||
* Identifier escape character
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $escapeChar = '`';
|
||||
|
||||
/**
|
||||
* MySQLi object
|
||||
*
|
||||
* Has to be preserved without being assigned to $conn_id.
|
||||
*
|
||||
* @var MySQLi
|
||||
*/
|
||||
public $mysqli;
|
||||
|
||||
/**
|
||||
* MySQLi constant
|
||||
*
|
||||
* For unbuffered queries use `MYSQLI_USE_RESULT`.
|
||||
*
|
||||
* Default mode for buffered queries uses `MYSQLI_STORE_RESULT`.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $resultMode = MYSQLI_STORE_RESULT;
|
||||
|
||||
/**
|
||||
* Connect to the database.
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function connect(bool $persistent = false)
|
||||
{
|
||||
// Do we have a socket path?
|
||||
if ($this->hostname[0] === '/') {
|
||||
$hostname = null;
|
||||
$port = null;
|
||||
$socket = $this->hostname;
|
||||
} else {
|
||||
$hostname = ($persistent === true) ? 'p:' . $this->hostname : $this->hostname;
|
||||
$port = empty($this->port) ? null : $this->port;
|
||||
$socket = '';
|
||||
}
|
||||
|
||||
$clientFlags = ($this->compress === true) ? MYSQLI_CLIENT_COMPRESS : 0;
|
||||
$this->mysqli = mysqli_init();
|
||||
|
||||
mysqli_report(MYSQLI_REPORT_ALL & ~MYSQLI_REPORT_INDEX);
|
||||
|
||||
$this->mysqli->options(MYSQLI_OPT_CONNECT_TIMEOUT, 10);
|
||||
|
||||
if (isset($this->strictOn)) {
|
||||
if ($this->strictOn) {
|
||||
$this->mysqli->options(
|
||||
MYSQLI_INIT_COMMAND,
|
||||
"SET SESSION sql_mode = CONCAT(@@sql_mode, ',', 'STRICT_ALL_TABLES')"
|
||||
);
|
||||
} else {
|
||||
$this->mysqli->options(
|
||||
MYSQLI_INIT_COMMAND,
|
||||
"SET SESSION sql_mode = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
|
||||
@@sql_mode,
|
||||
'STRICT_ALL_TABLES,', ''),
|
||||
',STRICT_ALL_TABLES', ''),
|
||||
'STRICT_ALL_TABLES', ''),
|
||||
'STRICT_TRANS_TABLES,', ''),
|
||||
',STRICT_TRANS_TABLES', ''),
|
||||
'STRICT_TRANS_TABLES', '')"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($this->encrypt)) {
|
||||
$ssl = [];
|
||||
|
||||
if (! empty($this->encrypt['ssl_key'])) {
|
||||
$ssl['key'] = $this->encrypt['ssl_key'];
|
||||
}
|
||||
if (! empty($this->encrypt['ssl_cert'])) {
|
||||
$ssl['cert'] = $this->encrypt['ssl_cert'];
|
||||
}
|
||||
if (! empty($this->encrypt['ssl_ca'])) {
|
||||
$ssl['ca'] = $this->encrypt['ssl_ca'];
|
||||
}
|
||||
if (! empty($this->encrypt['ssl_capath'])) {
|
||||
$ssl['capath'] = $this->encrypt['ssl_capath'];
|
||||
}
|
||||
if (! empty($this->encrypt['ssl_cipher'])) {
|
||||
$ssl['cipher'] = $this->encrypt['ssl_cipher'];
|
||||
}
|
||||
|
||||
if (! empty($ssl)) {
|
||||
if (isset($this->encrypt['ssl_verify'])) {
|
||||
if ($this->encrypt['ssl_verify']) {
|
||||
if (defined('MYSQLI_OPT_SSL_VERIFY_SERVER_CERT')) {
|
||||
$this->mysqli->options(MYSQLI_OPT_SSL_VERIFY_SERVER_CERT, 1);
|
||||
}
|
||||
}
|
||||
// Apparently (when it exists), setting MYSQLI_OPT_SSL_VERIFY_SERVER_CERT
|
||||
// to FALSE didn't do anything, so PHP 5.6.16 introduced yet another
|
||||
// constant ...
|
||||
//
|
||||
// https://secure.php.net/ChangeLog-5.php#5.6.16
|
||||
// https://bugs.php.net/bug.php?id=68344
|
||||
elseif (defined('MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT') && version_compare($this->mysqli->client_info, 'mysqlnd 5.6', '>=')) {
|
||||
$clientFlags += MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT;
|
||||
}
|
||||
}
|
||||
|
||||
$this->mysqli->ssl_set(
|
||||
$ssl['key'] ?? null,
|
||||
$ssl['cert'] ?? null,
|
||||
$ssl['ca'] ?? null,
|
||||
$ssl['capath'] ?? null,
|
||||
$ssl['cipher'] ?? null
|
||||
);
|
||||
}
|
||||
|
||||
$clientFlags += MYSQLI_CLIENT_SSL;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->mysqli->real_connect(
|
||||
$hostname,
|
||||
$this->username,
|
||||
$this->password,
|
||||
$this->database,
|
||||
$port,
|
||||
$socket,
|
||||
$clientFlags
|
||||
)) {
|
||||
// Prior to version 5.7.3, MySQL silently downgrades to an unencrypted connection if SSL setup fails
|
||||
if (($clientFlags & MYSQLI_CLIENT_SSL) && version_compare($this->mysqli->client_info, 'mysqlnd 5.7.3', '<=')
|
||||
&& empty($this->mysqli->query("SHOW STATUS LIKE 'ssl_cipher'")->fetch_object()->Value)
|
||||
) {
|
||||
$this->mysqli->close();
|
||||
$message = 'MySQLi was configured for an SSL connection, but got an unencrypted connection instead!';
|
||||
log_message('error', $message);
|
||||
|
||||
if ($this->DBDebug) {
|
||||
throw new DatabaseException($message);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! $this->mysqli->set_charset($this->charset)) {
|
||||
log_message('error', "Database: Unable to set the configured connection charset ('{$this->charset}').");
|
||||
|
||||
$this->mysqli->close();
|
||||
|
||||
if ($this->DBDebug) {
|
||||
throw new DatabaseException('Unable to set client connection character set: ' . $this->charset);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->mysqli;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
// Clean sensitive information from errors.
|
||||
$msg = $e->getMessage();
|
||||
|
||||
$msg = str_replace($this->username, '****', $msg);
|
||||
$msg = str_replace($this->password, '****', $msg);
|
||||
|
||||
throw new DatabaseException($msg, $e->getCode(), $e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep or establish the connection if no queries have been sent for
|
||||
* a length of time exceeding the server's idle timeout.
|
||||
*/
|
||||
public function reconnect()
|
||||
{
|
||||
$this->close();
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection.
|
||||
*/
|
||||
protected function _close()
|
||||
{
|
||||
$this->connID->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific database table to use.
|
||||
*/
|
||||
public function setDatabase(string $databaseName): bool
|
||||
{
|
||||
if ($databaseName === '') {
|
||||
$databaseName = $this->database;
|
||||
}
|
||||
|
||||
if (empty($this->connID)) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
if ($this->connID->select_db($databaseName)) {
|
||||
$this->database = $databaseName;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the version of the database being used.
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
if (isset($this->dataCache['version'])) {
|
||||
return $this->dataCache['version'];
|
||||
}
|
||||
|
||||
if (empty($this->mysqli)) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
return $this->dataCache['version'] = $this->mysqli->server_info;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the query against the database.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function execute(string $sql)
|
||||
{
|
||||
while ($this->connID->more_results()) {
|
||||
$this->connID->next_result();
|
||||
if ($res = $this->connID->store_result()) {
|
||||
$res->free();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->connID->query($this->prepQuery($sql), $this->resultMode);
|
||||
} catch (mysqli_sql_exception $e) {
|
||||
log_message('error', $e->getMessage());
|
||||
|
||||
if ($this->DBDebug) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prep the query. If needed, each database adapter can prep the query string
|
||||
*/
|
||||
protected function prepQuery(string $sql): string
|
||||
{
|
||||
// mysqli_affected_rows() returns 0 for "DELETE FROM TABLE" queries. This hack
|
||||
// modifies the query so that it a proper number of affected rows is returned.
|
||||
if ($this->deleteHack === true && preg_match('/^\s*DELETE\s+FROM\s+(\S+)\s*$/i', $sql)) {
|
||||
return trim($sql) . ' WHERE 1=1';
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of rows affected by this query.
|
||||
*/
|
||||
public function affectedRows(): int
|
||||
{
|
||||
return $this->connID->affected_rows ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-dependant string escape
|
||||
*/
|
||||
protected function _escapeString(string $str): string
|
||||
{
|
||||
if (! $this->connID) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
return $this->connID->real_escape_string($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape Like String Direct
|
||||
* There are a few instances where MySQLi queries cannot take the
|
||||
* additional "ESCAPE x" parameter for specifying the escape character
|
||||
* in "LIKE" strings, and this handles those directly with a backslash.
|
||||
*
|
||||
* @param string|string[] $str Input string
|
||||
*
|
||||
* @return string|string[]
|
||||
*/
|
||||
public function escapeLikeStringDirect($str)
|
||||
{
|
||||
if (is_array($str)) {
|
||||
foreach ($str as $key => $val) {
|
||||
$str[$key] = $this->escapeLikeStringDirect($val);
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
|
||||
$str = $this->_escapeString($str);
|
||||
|
||||
// Escape LIKE condition wildcards
|
||||
return str_replace(
|
||||
[$this->likeEscapeChar, '%', '_'],
|
||||
['\\' . $this->likeEscapeChar, '\\' . '%', '\\' . '_'],
|
||||
$str
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the SQL for listing tables in a platform-dependent manner.
|
||||
* Uses escapeLikeStringDirect().
|
||||
*/
|
||||
protected function _listTables(bool $prefixLimit = false): string
|
||||
{
|
||||
$sql = 'SHOW TABLES FROM ' . $this->escapeIdentifiers($this->database);
|
||||
|
||||
if ($prefixLimit !== false && $this->DBPrefix !== '') {
|
||||
return $sql . " LIKE '" . $this->escapeLikeStringDirect($this->DBPrefix) . "%'";
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific query string so that the column names can be fetched.
|
||||
*/
|
||||
protected function _listColumns(string $table = ''): string
|
||||
{
|
||||
return 'SHOW COLUMNS FROM ' . $this->protectIdentifiers($table, true, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with field data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _fieldData(string $table): array
|
||||
{
|
||||
$table = $this->protectIdentifiers($table, true, null, false);
|
||||
|
||||
if (($query = $this->query('SHOW COLUMNS FROM ' . $table)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetFieldData'));
|
||||
}
|
||||
$query = $query->getResultObject();
|
||||
|
||||
$retVal = [];
|
||||
|
||||
for ($i = 0, $c = count($query); $i < $c; $i++) {
|
||||
$retVal[$i] = new stdClass();
|
||||
$retVal[$i]->name = $query[$i]->Field;
|
||||
|
||||
sscanf($query[$i]->Type, '%[a-z](%d)', $retVal[$i]->type, $retVal[$i]->max_length);
|
||||
|
||||
$retVal[$i]->nullable = $query[$i]->Null === 'YES';
|
||||
$retVal[$i]->default = $query[$i]->Default;
|
||||
$retVal[$i]->primary_key = (int) ($query[$i]->Key === 'PRI');
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with index data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
* @throws LogicException
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _indexData(string $table): array
|
||||
{
|
||||
$table = $this->protectIdentifiers($table, true, null, false);
|
||||
|
||||
if (($query = $this->query('SHOW INDEX FROM ' . $table)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetIndexData'));
|
||||
}
|
||||
|
||||
if (! $indexes = $query->getResultArray()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$keys = [];
|
||||
|
||||
foreach ($indexes as $index) {
|
||||
if (empty($keys[$index['Key_name']])) {
|
||||
$keys[$index['Key_name']] = new stdClass();
|
||||
$keys[$index['Key_name']]->name = $index['Key_name'];
|
||||
|
||||
if ($index['Key_name'] === 'PRIMARY') {
|
||||
$type = 'PRIMARY';
|
||||
} elseif ($index['Index_type'] === 'FULLTEXT') {
|
||||
$type = 'FULLTEXT';
|
||||
} elseif ($index['Non_unique']) {
|
||||
$type = $index['Index_type'] === 'SPATIAL' ? 'SPATIAL' : 'INDEX';
|
||||
} else {
|
||||
$type = 'UNIQUE';
|
||||
}
|
||||
|
||||
$keys[$index['Key_name']]->type = $type;
|
||||
}
|
||||
|
||||
$keys[$index['Key_name']]->fields[] = $index['Column_name'];
|
||||
}
|
||||
|
||||
return $keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with Foreign key data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _foreignKeyData(string $table): array
|
||||
{
|
||||
$sql = '
|
||||
SELECT
|
||||
tc.CONSTRAINT_NAME,
|
||||
tc.TABLE_NAME,
|
||||
kcu.COLUMN_NAME,
|
||||
rc.REFERENCED_TABLE_NAME,
|
||||
kcu.REFERENCED_COLUMN_NAME
|
||||
FROM information_schema.TABLE_CONSTRAINTS AS tc
|
||||
INNER JOIN information_schema.REFERENTIAL_CONSTRAINTS AS rc
|
||||
ON tc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
||||
AND tc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
|
||||
INNER JOIN information_schema.KEY_COLUMN_USAGE AS kcu
|
||||
ON tc.CONSTRAINT_NAME = kcu.CONSTRAINT_NAME
|
||||
AND tc.CONSTRAINT_SCHEMA = kcu.CONSTRAINT_SCHEMA
|
||||
WHERE
|
||||
tc.CONSTRAINT_TYPE = ' . $this->escape('FOREIGN KEY') . ' AND
|
||||
tc.TABLE_SCHEMA = ' . $this->escape($this->database) . ' AND
|
||||
tc.TABLE_NAME = ' . $this->escape($table);
|
||||
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetForeignKeyData'));
|
||||
}
|
||||
$query = $query->getResultObject();
|
||||
|
||||
$retVal = [];
|
||||
|
||||
foreach ($query as $row) {
|
||||
$obj = new stdClass();
|
||||
$obj->constraint_name = $row->CONSTRAINT_NAME;
|
||||
$obj->table_name = $row->TABLE_NAME;
|
||||
$obj->column_name = $row->COLUMN_NAME;
|
||||
$obj->foreign_table_name = $row->REFERENCED_TABLE_NAME;
|
||||
$obj->foreign_column_name = $row->REFERENCED_COLUMN_NAME;
|
||||
|
||||
$retVal[] = $obj;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform-specific SQL to disable foreign key checks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _disableForeignKeyChecks()
|
||||
{
|
||||
return 'SET FOREIGN_KEY_CHECKS=0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform-specific SQL to enable foreign key checks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _enableForeignKeyChecks()
|
||||
{
|
||||
return 'SET FOREIGN_KEY_CHECKS=1';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last error code and message.
|
||||
* Must return this format: ['code' => string|int, 'message' => string]
|
||||
* intval(code) === 0 means "no error".
|
||||
*
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
public function error(): array
|
||||
{
|
||||
if (! empty($this->mysqli->connect_errno)) {
|
||||
return [
|
||||
'code' => $this->mysqli->connect_errno,
|
||||
'message' => $this->mysqli->connect_error,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => $this->connID->errno,
|
||||
'message' => $this->connID->error,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert ID
|
||||
*/
|
||||
public function insertID(): int
|
||||
{
|
||||
return $this->connID->insert_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin Transaction
|
||||
*/
|
||||
protected function _transBegin(): bool
|
||||
{
|
||||
$this->connID->autocommit(false);
|
||||
|
||||
return $this->connID->begin_transaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit Transaction
|
||||
*/
|
||||
protected function _transCommit(): bool
|
||||
{
|
||||
if ($this->connID->commit()) {
|
||||
$this->connID->autocommit(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback Transaction
|
||||
*/
|
||||
protected function _transRollback(): bool
|
||||
{
|
||||
if ($this->connID->rollback()) {
|
||||
$this->connID->autocommit(true);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
239
system/Database/MySQLi/Forge.php
Normal file
239
system/Database/MySQLi/Forge.php
Normal file
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\MySQLi;
|
||||
|
||||
use CodeIgniter\Database\Forge as BaseForge;
|
||||
|
||||
/**
|
||||
* Forge for MySQLi
|
||||
*/
|
||||
class Forge extends BaseForge
|
||||
{
|
||||
/**
|
||||
* CREATE DATABASE statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $createDatabaseStr = 'CREATE DATABASE %s CHARACTER SET %s COLLATE %s';
|
||||
|
||||
/**
|
||||
* CREATE DATABASE IF statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $createDatabaseIfStr = 'CREATE DATABASE IF NOT EXISTS %s CHARACTER SET %s COLLATE %s';
|
||||
|
||||
/**
|
||||
* DROP CONSTRAINT statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dropConstraintStr = 'ALTER TABLE %s DROP FOREIGN KEY %s';
|
||||
|
||||
/**
|
||||
* CREATE TABLE keys flag
|
||||
*
|
||||
* Whether table keys are created from within the
|
||||
* CREATE TABLE statement.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $createTableKeys = true;
|
||||
|
||||
/**
|
||||
* UNSIGNED support
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $_unsigned = [
|
||||
'TINYINT',
|
||||
'SMALLINT',
|
||||
'MEDIUMINT',
|
||||
'INT',
|
||||
'INTEGER',
|
||||
'BIGINT',
|
||||
'REAL',
|
||||
'DOUBLE',
|
||||
'DOUBLE PRECISION',
|
||||
'FLOAT',
|
||||
'DECIMAL',
|
||||
'NUMERIC',
|
||||
];
|
||||
|
||||
/**
|
||||
* Table Options list which required to be quoted
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $_quoted_table_options = [
|
||||
'COMMENT',
|
||||
'COMPRESSION',
|
||||
'CONNECTION',
|
||||
'DATA DIRECTORY',
|
||||
'INDEX DIRECTORY',
|
||||
'ENCRYPTION',
|
||||
'PASSWORD',
|
||||
];
|
||||
|
||||
/**
|
||||
* NULL value representation in CREATE/ALTER TABLE statements
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected $null = 'NULL';
|
||||
|
||||
/**
|
||||
* CREATE TABLE attributes
|
||||
*
|
||||
* @param array $attributes Associative array of table attributes
|
||||
*/
|
||||
protected function _createTableAttributes(array $attributes): string
|
||||
{
|
||||
$sql = '';
|
||||
|
||||
foreach (array_keys($attributes) as $key) {
|
||||
if (is_string($key)) {
|
||||
$sql .= ' ' . strtoupper($key) . ' = ';
|
||||
|
||||
if (in_array(strtoupper($key), $this->_quoted_table_options, true)) {
|
||||
$sql .= $this->db->escape($attributes[$key]);
|
||||
} else {
|
||||
$sql .= $this->db->escapeString($attributes[$key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($this->db->charset) && ! strpos($sql, 'CHARACTER SET') && ! strpos($sql, 'CHARSET')) {
|
||||
$sql .= ' DEFAULT CHARACTER SET = ' . $this->db->escapeString($this->db->charset);
|
||||
}
|
||||
|
||||
if (! empty($this->db->DBCollat) && ! strpos($sql, 'COLLATE')) {
|
||||
$sql .= ' COLLATE = ' . $this->db->escapeString($this->db->DBCollat);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* ALTER TABLE
|
||||
*
|
||||
* @param string $alterType ALTER type
|
||||
* @param string $table Table name
|
||||
* @param mixed $field Column definition
|
||||
*
|
||||
* @return string|string[]
|
||||
*/
|
||||
protected function _alterTable(string $alterType, string $table, $field)
|
||||
{
|
||||
if ($alterType === 'DROP') {
|
||||
return parent::_alterTable($alterType, $table, $field);
|
||||
}
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
|
||||
|
||||
foreach ($field as $i => $data) {
|
||||
if ($data['_literal'] !== false) {
|
||||
$field[$i] = ($alterType === 'ADD') ? "\n\tADD " . $data['_literal'] : "\n\tMODIFY " . $data['_literal'];
|
||||
} else {
|
||||
if ($alterType === 'ADD') {
|
||||
$field[$i]['_literal'] = "\n\tADD ";
|
||||
} else {
|
||||
$field[$i]['_literal'] = empty($data['new_name']) ? "\n\tMODIFY " : "\n\tCHANGE ";
|
||||
}
|
||||
|
||||
$field[$i] = $field[$i]['_literal'] . $this->_processColumn($field[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
return [$sql . implode(',', $field)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process column
|
||||
*/
|
||||
protected function _processColumn(array $field): string
|
||||
{
|
||||
$extraClause = isset($field['after']) ? ' AFTER ' . $this->db->escapeIdentifiers($field['after']) : '';
|
||||
|
||||
if (empty($extraClause) && isset($field['first']) && $field['first'] === true) {
|
||||
$extraClause = ' FIRST';
|
||||
}
|
||||
|
||||
return $this->db->escapeIdentifiers($field['name'])
|
||||
. (empty($field['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($field['new_name']))
|
||||
. ' ' . $field['type'] . $field['length']
|
||||
. $field['unsigned']
|
||||
. $field['null']
|
||||
. $field['default']
|
||||
. $field['auto_increment']
|
||||
. $field['unique']
|
||||
. (empty($field['comment']) ? '' : ' COMMENT ' . $field['comment'])
|
||||
. $extraClause;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process indexes
|
||||
*
|
||||
* @param string $table (ignored)
|
||||
*/
|
||||
protected function _processIndexes(string $table): string
|
||||
{
|
||||
$sql = '';
|
||||
|
||||
for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
|
||||
if (is_array($this->keys[$i])) {
|
||||
for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) {
|
||||
if (! isset($this->fields[$this->keys[$i][$i2]])) {
|
||||
unset($this->keys[$i][$i2]);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
} elseif (! isset($this->fields[$this->keys[$i]])) {
|
||||
unset($this->keys[$i]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! is_array($this->keys[$i])) {
|
||||
$this->keys[$i] = [$this->keys[$i]];
|
||||
}
|
||||
|
||||
$unique = in_array($i, $this->uniqueKeys, true) ? 'UNIQUE ' : '';
|
||||
|
||||
$sql .= ",\n\t{$unique}KEY " . $this->db->escapeIdentifiers(implode('_', $this->keys[$i]))
|
||||
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ')';
|
||||
}
|
||||
|
||||
$this->keys = [];
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop Key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function dropKey(string $table, string $keyName)
|
||||
{
|
||||
$sql = sprintf(
|
||||
$this->dropIndexStr,
|
||||
$this->db->escapeIdentifiers($keyName),
|
||||
$this->db->escapeIdentifiers($this->db->DBPrefix . $table),
|
||||
);
|
||||
|
||||
return $this->db->query($sql);
|
||||
}
|
||||
}
|
||||
87
system/Database/MySQLi/PreparedQuery.php
Normal file
87
system/Database/MySQLi/PreparedQuery.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\MySQLi;
|
||||
|
||||
use BadMethodCallException;
|
||||
use CodeIgniter\Database\BasePreparedQuery;
|
||||
|
||||
/**
|
||||
* Prepared query for MySQLi
|
||||
*/
|
||||
class PreparedQuery extends BasePreparedQuery
|
||||
{
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* NOTE: This version is based on SQL code. Child classes should
|
||||
* override this method.
|
||||
*
|
||||
* @param array $options Passed to the connection's prepare statement.
|
||||
* Unused in the MySQLi driver.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _prepare(string $sql, array $options = [])
|
||||
{
|
||||
// Mysqli driver doesn't like statements
|
||||
// with terminating semicolons.
|
||||
$sql = rtrim($sql, ';');
|
||||
|
||||
if (! $this->statement = $this->db->mysqli->prepare($sql)) {
|
||||
$this->errorCode = $this->db->mysqli->errno;
|
||||
$this->errorString = $this->db->mysqli->error;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*/
|
||||
public function _execute(array $data): bool
|
||||
{
|
||||
if (! isset($this->statement)) {
|
||||
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
|
||||
}
|
||||
|
||||
// First off -bind the parameters
|
||||
$bindTypes = '';
|
||||
|
||||
// Determine the type string
|
||||
foreach ($data as $item) {
|
||||
if (is_int($item)) {
|
||||
$bindTypes .= 'i';
|
||||
} elseif (is_numeric($item)) {
|
||||
$bindTypes .= 'd';
|
||||
} else {
|
||||
$bindTypes .= 's';
|
||||
}
|
||||
}
|
||||
|
||||
// Bind it
|
||||
$this->statement->bind_param($bindTypes, ...$data);
|
||||
|
||||
return $this->statement->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result object for the prepared query.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _getResult()
|
||||
{
|
||||
return $this->statement->get_result();
|
||||
}
|
||||
}
|
||||
162
system/Database/MySQLi/Result.php
Normal file
162
system/Database/MySQLi/Result.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\MySQLi;
|
||||
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Result for MySQLi
|
||||
*/
|
||||
class Result extends BaseResult
|
||||
{
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getFieldCount(): int
|
||||
{
|
||||
return $this->resultID->field_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
public function getFieldNames(): array
|
||||
{
|
||||
$fieldNames = [];
|
||||
$this->resultID->field_seek(0);
|
||||
|
||||
while ($field = $this->resultID->fetch_field()) {
|
||||
$fieldNames[] = $field->name;
|
||||
}
|
||||
|
||||
return $fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
public function getFieldData(): array
|
||||
{
|
||||
static $dataTypes = [
|
||||
MYSQLI_TYPE_DECIMAL => 'decimal',
|
||||
MYSQLI_TYPE_NEWDECIMAL => 'newdecimal',
|
||||
MYSQLI_TYPE_FLOAT => 'float',
|
||||
MYSQLI_TYPE_DOUBLE => 'double',
|
||||
|
||||
MYSQLI_TYPE_BIT => 'bit',
|
||||
MYSQLI_TYPE_SHORT => 'short',
|
||||
MYSQLI_TYPE_LONG => 'long',
|
||||
MYSQLI_TYPE_LONGLONG => 'longlong',
|
||||
MYSQLI_TYPE_INT24 => 'int24',
|
||||
|
||||
MYSQLI_TYPE_YEAR => 'year',
|
||||
|
||||
MYSQLI_TYPE_TIMESTAMP => 'timestamp',
|
||||
MYSQLI_TYPE_DATE => 'date',
|
||||
MYSQLI_TYPE_TIME => 'time',
|
||||
MYSQLI_TYPE_DATETIME => 'datetime',
|
||||
MYSQLI_TYPE_NEWDATE => 'newdate',
|
||||
|
||||
MYSQLI_TYPE_SET => 'set',
|
||||
|
||||
MYSQLI_TYPE_VAR_STRING => 'var_string',
|
||||
MYSQLI_TYPE_STRING => 'string',
|
||||
|
||||
MYSQLI_TYPE_GEOMETRY => 'geometry',
|
||||
MYSQLI_TYPE_TINY_BLOB => 'tiny_blob',
|
||||
MYSQLI_TYPE_MEDIUM_BLOB => 'medium_blob',
|
||||
MYSQLI_TYPE_LONG_BLOB => 'long_blob',
|
||||
MYSQLI_TYPE_BLOB => 'blob',
|
||||
];
|
||||
|
||||
$retVal = [];
|
||||
$fieldData = $this->resultID->fetch_fields();
|
||||
|
||||
foreach ($fieldData as $i => $data) {
|
||||
$retVal[$i] = new stdClass();
|
||||
$retVal[$i]->name = $data->name;
|
||||
$retVal[$i]->type = $data->type;
|
||||
$retVal[$i]->type_name = in_array($data->type, [1, 247], true) ? 'char' : ($dataTypes[$data->type] ?? null);
|
||||
$retVal[$i]->max_length = $data->max_length;
|
||||
$retVal[$i]->primary_key = $data->flags & 2;
|
||||
$retVal[$i]->length = $data->length;
|
||||
$retVal[$i]->default = $data->def;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*/
|
||||
public function freeResult()
|
||||
{
|
||||
if (is_object($this->resultID)) {
|
||||
$this->resultID->free();
|
||||
$this->resultID = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function dataSeek(int $n = 0)
|
||||
{
|
||||
return $this->resultID->data_seek($n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an array.
|
||||
*
|
||||
* Overridden by driver classes.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fetchAssoc()
|
||||
{
|
||||
return $this->resultID->fetch_assoc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an object.
|
||||
*
|
||||
* Overridden by child classes.
|
||||
*
|
||||
* @return bool|Entity|object
|
||||
*/
|
||||
protected function fetchObject(string $className = 'stdClass')
|
||||
{
|
||||
if (is_subclass_of($className, Entity::class)) {
|
||||
return empty($data = $this->fetchAssoc()) ? false : (new $className())->setAttributes($data);
|
||||
}
|
||||
|
||||
return $this->resultID->fetch_object($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of rows in the resultID (i.e., mysqli_result object)
|
||||
*/
|
||||
public function getNumRows(): int
|
||||
{
|
||||
if (! is_int($this->numRows)) {
|
||||
$this->numRows = $this->resultID->num_rows;
|
||||
}
|
||||
|
||||
return $this->numRows;
|
||||
}
|
||||
}
|
||||
45
system/Database/MySQLi/Utils.php
Normal file
45
system/Database/MySQLi/Utils.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\MySQLi;
|
||||
|
||||
use CodeIgniter\Database\BaseUtils;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* Utils for MySQLi
|
||||
*/
|
||||
class Utils extends BaseUtils
|
||||
{
|
||||
/**
|
||||
* List databases statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $listDatabases = 'SHOW DATABASES';
|
||||
|
||||
/**
|
||||
* OPTIMIZE TABLE statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $optimizeTable = 'OPTIMIZE TABLE %s';
|
||||
|
||||
/**
|
||||
* Platform dependent version of the backup function.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _backup(?array $prefs = null)
|
||||
{
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
}
|
||||
313
system/Database/Postgre/Builder.php
Normal file
313
system/Database/Postgre/Builder.php
Normal file
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* Builder for Postgre
|
||||
*/
|
||||
class Builder extends BaseBuilder
|
||||
{
|
||||
/**
|
||||
* ORDER BY random keyword
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $randomKeyword = [
|
||||
'RANDOM()',
|
||||
];
|
||||
|
||||
/**
|
||||
* Specifies which sql statements
|
||||
* support the ignore option.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $supportedIgnoreStatements = [
|
||||
'insert' => 'ON CONFLICT DO NOTHING',
|
||||
];
|
||||
|
||||
/**
|
||||
* Checks if the ignore option is supported by
|
||||
* the Database Driver for the specific statement.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function compileIgnore(string $statement)
|
||||
{
|
||||
$sql = parent::compileIgnore($statement);
|
||||
|
||||
if (! empty($sql)) {
|
||||
$sql = ' ' . trim($sql);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* ORDER BY
|
||||
*
|
||||
* @param string $direction ASC, DESC or RANDOM
|
||||
*
|
||||
* @return BaseBuilder
|
||||
*/
|
||||
public function orderBy(string $orderBy, string $direction = '', ?bool $escape = null)
|
||||
{
|
||||
$direction = strtoupper(trim($direction));
|
||||
if ($direction === 'RANDOM') {
|
||||
if (ctype_digit($orderBy)) {
|
||||
$orderBy = (float) ($orderBy > 1 ? "0.{$orderBy}" : $orderBy);
|
||||
}
|
||||
|
||||
if (is_float($orderBy)) {
|
||||
$this->db->simpleQuery("SET SEED {$orderBy}");
|
||||
}
|
||||
|
||||
$orderBy = $this->randomKeyword[0];
|
||||
$direction = '';
|
||||
$escape = false;
|
||||
}
|
||||
|
||||
return parent::orderBy($orderBy, $direction, $escape);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments a numeric column by the specified value.
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function increment(string $column, int $value = 1)
|
||||
{
|
||||
$column = $this->db->protectIdentifiers($column);
|
||||
|
||||
$sql = $this->_update($this->QBFrom[0], [$column => "to_number({$column}, '9999999') + {$value}"]);
|
||||
|
||||
return $this->db->query($sql, $this->binds, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements a numeric column by the specified value.
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function decrement(string $column, int $value = 1)
|
||||
{
|
||||
$column = $this->db->protectIdentifiers($column);
|
||||
|
||||
$sql = $this->_update($this->QBFrom[0], [$column => "to_number({$column}, '9999999') - {$value}"]);
|
||||
|
||||
return $this->db->query($sql, $this->binds, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles an replace into string and runs the query.
|
||||
* Because PostgreSQL doesn't support the replace into command,
|
||||
* we simply do a DELETE and an INSERT on the first key/value
|
||||
* combo, assuming that it's either the primary key or a unique key.
|
||||
*
|
||||
* @param array|null $set An associative array of insert values
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function replace(?array $set = null)
|
||||
{
|
||||
if ($set !== null) {
|
||||
$this->set($set);
|
||||
}
|
||||
|
||||
if (! $this->QBSet) {
|
||||
if (CI_DEBUG) {
|
||||
throw new DatabaseException('You must use the "set" method to update an entry.');
|
||||
}
|
||||
|
||||
return false; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$table = $this->QBFrom[0];
|
||||
$set = $this->binds;
|
||||
|
||||
array_walk($set, static function (array &$item) {
|
||||
$item = $item[0];
|
||||
});
|
||||
|
||||
$key = array_key_first($set);
|
||||
$value = $set[$key];
|
||||
|
||||
$builder = $this->db->table($table);
|
||||
$exists = $builder->where($key, $value, true)->get()->getFirstRow();
|
||||
|
||||
if (empty($exists) && $this->testMode) {
|
||||
$result = $this->getCompiledInsert();
|
||||
} elseif (empty($exists)) {
|
||||
$result = $builder->insert($set);
|
||||
} elseif ($this->testMode) {
|
||||
$result = $this->where($key, $value, true)->getCompiledUpdate();
|
||||
} else {
|
||||
array_shift($set);
|
||||
$result = $builder->where($key, $value, true)->update($set);
|
||||
}
|
||||
|
||||
unset($builder);
|
||||
$this->resetWrite();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific insert string from the supplied data
|
||||
*/
|
||||
protected function _insert(string $table, array $keys, array $unescapedKeys): string
|
||||
{
|
||||
return trim(sprintf('INSERT INTO %s (%s) VALUES (%s) %s', $table, implode(', ', $keys), implode(', ', $unescapedKeys), $this->compileIgnore('insert')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific insert string from the supplied data.
|
||||
*/
|
||||
protected function _insertBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
return trim(sprintf('INSERT INTO %s (%s) VALUES %s %s', $table, implode(', ', $keys), implode(', ', $values), $this->compileIgnore('insert')));
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles a delete string and runs the query
|
||||
*
|
||||
* @param mixed $where
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete($where = '', ?int $limit = null, bool $resetData = true)
|
||||
{
|
||||
if (! empty($limit) || ! empty($this->QBLimit)) {
|
||||
throw new DatabaseException('PostgreSQL does not allow LIMITs on DELETE queries.');
|
||||
}
|
||||
|
||||
return parent::delete($where, $limit, $resetData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific LIMIT clause.
|
||||
*/
|
||||
protected function _limit(string $sql, bool $offsetIgnore = false): string
|
||||
{
|
||||
return $sql . ' LIMIT ' . $this->QBLimit . ($this->QBOffset ? " OFFSET {$this->QBOffset}" : '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific update string from the supplied data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
protected function _update(string $table, array $values): string
|
||||
{
|
||||
if (! empty($this->QBLimit)) {
|
||||
throw new DatabaseException('Postgres does not support LIMITs with UPDATE queries.');
|
||||
}
|
||||
|
||||
$this->QBOrderBy = [];
|
||||
|
||||
return parent::_update($table, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific batch update string from the supplied data
|
||||
*/
|
||||
protected function _updateBatch(string $table, array $values, string $index): string
|
||||
{
|
||||
$ids = [];
|
||||
$final = [];
|
||||
|
||||
foreach ($values as $val) {
|
||||
$ids[] = $val[$index];
|
||||
|
||||
foreach (array_keys($val) as $field) {
|
||||
if ($field !== $index) {
|
||||
$final[$field] = $final[$field] ?? [];
|
||||
|
||||
$final[$field][] = "WHEN {$val[$index]} THEN {$val[$field]}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cases = '';
|
||||
|
||||
foreach ($final as $k => $v) {
|
||||
$cases .= "{$k} = (CASE {$index}\n"
|
||||
. implode("\n", $v)
|
||||
. "\nELSE {$k} END), ";
|
||||
}
|
||||
|
||||
$this->where("{$index} IN(" . implode(',', $ids) . ')', null, false);
|
||||
|
||||
return "UPDATE {$table} SET " . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific delete string from the supplied data
|
||||
*/
|
||||
protected function _delete(string $table): string
|
||||
{
|
||||
$this->QBLimit = false;
|
||||
|
||||
return parent::_delete($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific truncate string from the supplied data
|
||||
*
|
||||
* If the database does not support the truncate() command,
|
||||
* then this method maps to 'DELETE FROM table'
|
||||
*/
|
||||
protected function _truncate(string $table): string
|
||||
{
|
||||
return 'TRUNCATE ' . $table . ' RESTART IDENTITY';
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform independent LIKE statement builder.
|
||||
*
|
||||
* In PostgreSQL, the ILIKE operator will perform case insensitive
|
||||
* searches according to the current locale.
|
||||
*
|
||||
* @see https://www.postgresql.org/docs/9.2/static/functions-matching.html
|
||||
*/
|
||||
protected function _like_statement(?string $prefix, string $column, ?string $not, string $bind, bool $insensitiveSearch = false): string
|
||||
{
|
||||
$op = $insensitiveSearch === true ? 'ILIKE' : 'LIKE';
|
||||
|
||||
return "{$prefix} {$column} {$not} {$op} :{$bind}:";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the JOIN portion of the query
|
||||
*
|
||||
* @return BaseBuilder
|
||||
*/
|
||||
public function join(string $table, string $cond, string $type = '', ?bool $escape = null)
|
||||
{
|
||||
if (! in_array('FULL OUTER', $this->joinTypes, true)) {
|
||||
$this->joinTypes = array_merge($this->joinTypes, ['FULL OUTER']);
|
||||
}
|
||||
|
||||
return parent::join($table, $cond, $type, $escape);
|
||||
}
|
||||
}
|
||||
517
system/Database/Postgre/Connection.php
Normal file
517
system/Database/Postgre/Connection.php
Normal file
@@ -0,0 +1,517 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use ErrorException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Connection for Postgre
|
||||
*/
|
||||
class Connection extends BaseConnection
|
||||
{
|
||||
/**
|
||||
* Database driver
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $DBDriver = 'Postgre';
|
||||
|
||||
/**
|
||||
* Database schema
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $schema = 'public';
|
||||
|
||||
/**
|
||||
* Identifier escape character
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $escapeChar = '"';
|
||||
|
||||
/**
|
||||
* Connect to the database.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function connect(bool $persistent = false)
|
||||
{
|
||||
if (empty($this->DSN)) {
|
||||
$this->buildDSN();
|
||||
}
|
||||
|
||||
// Strip pgsql if exists
|
||||
if (mb_strpos($this->DSN, 'pgsql:') === 0) {
|
||||
$this->DSN = mb_substr($this->DSN, 6);
|
||||
}
|
||||
|
||||
// Convert semicolons to spaces.
|
||||
$this->DSN = str_replace(';', ' ', $this->DSN);
|
||||
|
||||
$this->connID = $persistent === true ? pg_pconnect($this->DSN) : pg_connect($this->DSN);
|
||||
|
||||
if ($this->connID !== false) {
|
||||
if ($persistent === true && pg_connection_status($this->connID) === PGSQL_CONNECTION_BAD && pg_ping($this->connID) === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! empty($this->schema)) {
|
||||
$this->simpleQuery("SET search_path TO {$this->schema},public");
|
||||
}
|
||||
|
||||
if ($this->setClientEncoding($this->charset) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->connID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep or establish the connection if no queries have been sent for
|
||||
* a length of time exceeding the server's idle timeout.
|
||||
*/
|
||||
public function reconnect()
|
||||
{
|
||||
if (pg_ping($this->connID) === false) {
|
||||
$this->connID = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection.
|
||||
*/
|
||||
protected function _close()
|
||||
{
|
||||
pg_close($this->connID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific database table to use.
|
||||
*/
|
||||
public function setDatabase(string $databaseName): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the version of the database being used.
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
if (isset($this->dataCache['version'])) {
|
||||
return $this->dataCache['version'];
|
||||
}
|
||||
|
||||
if (! $this->connID || ($pgVersion = pg_version($this->connID)) === false) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
return isset($pgVersion['server']) ? $this->dataCache['version'] = $pgVersion['server'] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the query against the database.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function execute(string $sql)
|
||||
{
|
||||
try {
|
||||
return pg_query($this->connID, $sql);
|
||||
} catch (ErrorException $e) {
|
||||
log_message('error', $e);
|
||||
if ($this->DBDebug) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the prefix of the function to access the DB.
|
||||
*/
|
||||
protected function getDriverFunctionPrefix(): string
|
||||
{
|
||||
return 'pg_';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of rows affected by this query.
|
||||
*/
|
||||
public function affectedRows(): int
|
||||
{
|
||||
return pg_affected_rows($this->resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* "Smart" Escape String
|
||||
*
|
||||
* Escapes data based on type
|
||||
*
|
||||
* @param mixed $str
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function escape($str)
|
||||
{
|
||||
if (! $this->connID) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
if (is_string($str) || (is_object($str) && method_exists($str, '__toString'))) {
|
||||
return pg_escape_literal($this->connID, $str);
|
||||
}
|
||||
|
||||
if (is_bool($str)) {
|
||||
return $str ? 'TRUE' : 'FALSE';
|
||||
}
|
||||
|
||||
return parent::escape($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-dependant string escape
|
||||
*/
|
||||
protected function _escapeString(string $str): string
|
||||
{
|
||||
if (! $this->connID) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
return pg_escape_string($this->connID, $str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the SQL for listing tables in a platform-dependent manner.
|
||||
*/
|
||||
protected function _listTables(bool $prefixLimit = false): string
|
||||
{
|
||||
$sql = 'SELECT "table_name" FROM "information_schema"."tables" WHERE "table_schema" = \'' . $this->schema . "'";
|
||||
|
||||
if ($prefixLimit !== false && $this->DBPrefix !== '') {
|
||||
return $sql . ' AND "table_name" LIKE \''
|
||||
. $this->escapeLikeString($this->DBPrefix) . "%' "
|
||||
. sprintf($this->likeEscapeStr, $this->likeEscapeChar);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific query string so that the column names can be fetched.
|
||||
*/
|
||||
protected function _listColumns(string $table = ''): string
|
||||
{
|
||||
return 'SELECT "column_name"
|
||||
FROM "information_schema"."columns"
|
||||
WHERE LOWER("table_name") = '
|
||||
. $this->escape($this->DBPrefix . strtolower($table))
|
||||
. ' ORDER BY "ordinal_position"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with field data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _fieldData(string $table): array
|
||||
{
|
||||
$sql = 'SELECT "column_name", "data_type", "character_maximum_length", "numeric_precision", "column_default"
|
||||
FROM "information_schema"."columns"
|
||||
WHERE LOWER("table_name") = '
|
||||
. $this->escape(strtolower($table))
|
||||
. ' ORDER BY "ordinal_position"';
|
||||
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetFieldData'));
|
||||
}
|
||||
$query = $query->getResultObject();
|
||||
|
||||
$retVal = [];
|
||||
|
||||
for ($i = 0, $c = count($query); $i < $c; $i++) {
|
||||
$retVal[$i] = new stdClass();
|
||||
|
||||
$retVal[$i]->name = $query[$i]->column_name;
|
||||
$retVal[$i]->type = $query[$i]->data_type;
|
||||
$retVal[$i]->default = $query[$i]->column_default;
|
||||
$retVal[$i]->max_length = $query[$i]->character_maximum_length > 0 ? $query[$i]->character_maximum_length : $query[$i]->numeric_precision;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with index data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _indexData(string $table): array
|
||||
{
|
||||
$sql = 'SELECT "indexname", "indexdef"
|
||||
FROM "pg_indexes"
|
||||
WHERE LOWER("tablename") = ' . $this->escape(strtolower($table)) . '
|
||||
AND "schemaname" = ' . $this->escape('public');
|
||||
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetIndexData'));
|
||||
}
|
||||
$query = $query->getResultObject();
|
||||
|
||||
$retVal = [];
|
||||
|
||||
foreach ($query as $row) {
|
||||
$obj = new stdClass();
|
||||
$obj->name = $row->indexname;
|
||||
$_fields = explode(',', preg_replace('/^.*\((.+?)\)$/', '$1', trim($row->indexdef)));
|
||||
$obj->fields = array_map(static function ($v) {
|
||||
return trim($v);
|
||||
}, $_fields);
|
||||
|
||||
if (strpos($row->indexdef, 'CREATE UNIQUE INDEX pk') === 0) {
|
||||
$obj->type = 'PRIMARY';
|
||||
} else {
|
||||
$obj->type = (strpos($row->indexdef, 'CREATE UNIQUE') === 0) ? 'UNIQUE' : 'INDEX';
|
||||
}
|
||||
|
||||
$retVal[$obj->name] = $obj;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with Foreign key data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _foreignKeyData(string $table): array
|
||||
{
|
||||
$sql = 'SELECT
|
||||
tc.constraint_name, tc.table_name, kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
JOIN information_schema.key_column_usage AS kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
JOIN information_schema.constraint_column_usage AS ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
WHERE constraint_type = ' . $this->escape('FOREIGN KEY') . ' AND
|
||||
tc.table_name = ' . $this->escape($table);
|
||||
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetForeignKeyData'));
|
||||
}
|
||||
|
||||
$query = $query->getResultObject();
|
||||
$retVal = [];
|
||||
|
||||
foreach ($query as $row) {
|
||||
$obj = new stdClass();
|
||||
|
||||
$obj->constraint_name = $row->constraint_name;
|
||||
$obj->table_name = $row->table_name;
|
||||
$obj->column_name = $row->column_name;
|
||||
$obj->foreign_table_name = $row->foreign_table_name;
|
||||
$obj->foreign_column_name = $row->foreign_column_name;
|
||||
|
||||
$retVal[] = $obj;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform-specific SQL to disable foreign key checks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _disableForeignKeyChecks()
|
||||
{
|
||||
return 'SET CONSTRAINTS ALL DEFERRED';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform-specific SQL to enable foreign key checks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _enableForeignKeyChecks()
|
||||
{
|
||||
return 'SET CONSTRAINTS ALL IMMEDIATE;';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last error code and message.
|
||||
* Must return this format: ['code' => string|int, 'message' => string]
|
||||
* intval(code) === 0 means "no error".
|
||||
*
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
public function error(): array
|
||||
{
|
||||
return [
|
||||
'code' => '',
|
||||
'message' => pg_last_error($this->connID) ?: '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|string
|
||||
*/
|
||||
public function insertID()
|
||||
{
|
||||
$v = pg_version($this->connID);
|
||||
// 'server' key is only available since PostgreSQL 7.4
|
||||
$v = explode(' ', $v['server'])[0] ?? 0;
|
||||
|
||||
$table = func_num_args() > 0 ? func_get_arg(0) : null;
|
||||
$column = func_num_args() > 1 ? func_get_arg(1) : null;
|
||||
|
||||
if ($table === null && $v >= '8.1') {
|
||||
$sql = 'SELECT LASTVAL() AS ins_id';
|
||||
} elseif ($table !== null) {
|
||||
if ($column !== null && $v >= '8.0') {
|
||||
$sql = "SELECT pg_get_serial_sequence('{$table}', '{$column}') AS seq";
|
||||
$query = $this->query($sql);
|
||||
$query = $query->getRow();
|
||||
$seq = $query->seq;
|
||||
} else {
|
||||
// seq_name passed in table parameter
|
||||
$seq = $table;
|
||||
}
|
||||
|
||||
$sql = "SELECT CURRVAL('{$seq}') AS ins_id";
|
||||
} else {
|
||||
return pg_last_oid($this->resultID);
|
||||
}
|
||||
|
||||
$query = $this->query($sql);
|
||||
$query = $query->getRow();
|
||||
|
||||
return (int) $query->ins_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a DSN from the provided parameters
|
||||
*/
|
||||
protected function buildDSN()
|
||||
{
|
||||
if ($this->DSN !== '') {
|
||||
$this->DSN = '';
|
||||
}
|
||||
|
||||
// If UNIX sockets are used, we shouldn't set a port
|
||||
if (strpos($this->hostname, '/') !== false) {
|
||||
$this->port = '';
|
||||
}
|
||||
|
||||
if ($this->hostname !== '') {
|
||||
$this->DSN = "host={$this->hostname} ";
|
||||
}
|
||||
|
||||
// ctype_digit only accepts strings
|
||||
$port = (string) $this->port;
|
||||
|
||||
if ($port !== '' && ctype_digit($port)) {
|
||||
$this->DSN .= "port={$port} ";
|
||||
}
|
||||
|
||||
if ($this->username !== '') {
|
||||
$this->DSN .= "user={$this->username} ";
|
||||
|
||||
// An empty password is valid!
|
||||
// password must be set to null to ignore it.
|
||||
if ($this->password !== null) {
|
||||
$this->DSN .= "password='{$this->password}' ";
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->database !== '') {
|
||||
$this->DSN .= "dbname={$this->database} ";
|
||||
}
|
||||
|
||||
// We don't have these options as elements in our standard configuration
|
||||
// array, but they might be set by parse_url() if the configuration was
|
||||
// provided via string> Example:
|
||||
//
|
||||
// Postgre://username:password@localhost:5432/database?connect_timeout=5&sslmode=1
|
||||
foreach (['connect_timeout', 'options', 'sslmode', 'service'] as $key) {
|
||||
if (isset($this->{$key}) && is_string($this->{$key}) && $this->{$key} !== '') {
|
||||
$this->DSN .= "{$key}='{$this->{$key}}' ";
|
||||
}
|
||||
}
|
||||
|
||||
$this->DSN = rtrim($this->DSN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set client encoding
|
||||
*/
|
||||
protected function setClientEncoding(string $charset): bool
|
||||
{
|
||||
return pg_set_client_encoding($this->connID, $charset) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin Transaction
|
||||
*/
|
||||
protected function _transBegin(): bool
|
||||
{
|
||||
return (bool) pg_query($this->connID, 'BEGIN');
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit Transaction
|
||||
*/
|
||||
protected function _transCommit(): bool
|
||||
{
|
||||
return (bool) pg_query($this->connID, 'COMMIT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback Transaction
|
||||
*/
|
||||
protected function _transRollback(): bool
|
||||
{
|
||||
return (bool) pg_query($this->connID, 'ROLLBACK');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a query is a "write" type.
|
||||
*
|
||||
* Overrides BaseConnection::isWriteType, adding additional read query types.
|
||||
*
|
||||
* @param mixed $sql
|
||||
*/
|
||||
public function isWriteType($sql): bool
|
||||
{
|
||||
if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parent::isWriteType($sql);
|
||||
}
|
||||
}
|
||||
193
system/Database/Postgre/Forge.php
Normal file
193
system/Database/Postgre/Forge.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use CodeIgniter\Database\Forge as BaseForge;
|
||||
|
||||
/**
|
||||
* Forge for Postgre
|
||||
*/
|
||||
class Forge extends BaseForge
|
||||
{
|
||||
/**
|
||||
* CHECK DATABASE EXIST statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $checkDatabaseExistStr = 'SELECT 1 FROM pg_database WHERE datname = ?';
|
||||
|
||||
/**
|
||||
* DROP CONSTRAINT statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dropConstraintStr = 'ALTER TABLE %s DROP CONSTRAINT %s';
|
||||
|
||||
/**
|
||||
* DROP INDEX statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dropIndexStr = 'DROP INDEX %s';
|
||||
|
||||
/**
|
||||
* UNSIGNED support
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $_unsigned = [
|
||||
'INT2' => 'INTEGER',
|
||||
'SMALLINT' => 'INTEGER',
|
||||
'INT' => 'BIGINT',
|
||||
'INT4' => 'BIGINT',
|
||||
'INTEGER' => 'BIGINT',
|
||||
'INT8' => 'NUMERIC',
|
||||
'BIGINT' => 'NUMERIC',
|
||||
'REAL' => 'DOUBLE PRECISION',
|
||||
'FLOAT' => 'DOUBLE PRECISION',
|
||||
];
|
||||
|
||||
/**
|
||||
* NULL value representation in CREATE/ALTER TABLE statements
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected $null = 'NULL';
|
||||
|
||||
/**
|
||||
* CREATE TABLE attributes
|
||||
*
|
||||
* @param array $attributes Associative array of table attributes
|
||||
*/
|
||||
protected function _createTableAttributes(array $attributes): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $field
|
||||
*
|
||||
* @return array|bool|string
|
||||
*/
|
||||
protected function _alterTable(string $alterType, string $table, $field)
|
||||
{
|
||||
if (in_array($alterType, ['DROP', 'ADD'], true)) {
|
||||
return parent::_alterTable($alterType, $table, $field);
|
||||
}
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table);
|
||||
$sqls = [];
|
||||
|
||||
foreach ($field as $data) {
|
||||
if ($data['_literal'] !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (version_compare($this->db->getVersion(), '8', '>=') && isset($data['type'])) {
|
||||
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
|
||||
. " TYPE {$data['type']}{$data['length']}";
|
||||
}
|
||||
|
||||
if (! empty($data['default'])) {
|
||||
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
|
||||
. " SET DEFAULT {$data['default']}";
|
||||
}
|
||||
|
||||
if (isset($data['null'])) {
|
||||
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
|
||||
. ($data['null'] === true ? ' DROP' : ' SET') . ' NOT NULL';
|
||||
}
|
||||
|
||||
if (! empty($data['new_name'])) {
|
||||
$sqls[] = $sql . ' RENAME COLUMN ' . $this->db->escapeIdentifiers($data['name'])
|
||||
. ' TO ' . $this->db->escapeIdentifiers($data['new_name']);
|
||||
}
|
||||
|
||||
if (! empty($data['comment'])) {
|
||||
$sqls[] = 'COMMENT ON COLUMN' . $this->db->escapeIdentifiers($table)
|
||||
. '.' . $this->db->escapeIdentifiers($data['name'])
|
||||
. " IS {$data['comment']}";
|
||||
}
|
||||
}
|
||||
|
||||
return $sqls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process column
|
||||
*/
|
||||
protected function _processColumn(array $field): string
|
||||
{
|
||||
return $this->db->escapeIdentifiers($field['name'])
|
||||
. ' ' . $field['type'] . $field['length']
|
||||
. $field['default']
|
||||
. $field['null']
|
||||
. $field['auto_increment']
|
||||
. $field['unique'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a data type mapping between different databases.
|
||||
*/
|
||||
protected function _attributeType(array &$attributes)
|
||||
{
|
||||
// Reset field lengths for data types that don't support it
|
||||
if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== false) {
|
||||
$attributes['CONSTRAINT'] = null;
|
||||
}
|
||||
|
||||
switch (strtoupper($attributes['TYPE'])) {
|
||||
case 'TINYINT':
|
||||
$attributes['TYPE'] = 'SMALLINT';
|
||||
$attributes['UNSIGNED'] = false;
|
||||
break;
|
||||
|
||||
case 'MEDIUMINT':
|
||||
$attributes['TYPE'] = 'INTEGER';
|
||||
$attributes['UNSIGNED'] = false;
|
||||
break;
|
||||
|
||||
case 'DATETIME':
|
||||
$attributes['TYPE'] = 'TIMESTAMP';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Field attribute AUTO_INCREMENT
|
||||
*/
|
||||
protected function _attributeAutoIncrement(array &$attributes, array &$field)
|
||||
{
|
||||
if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true) {
|
||||
$field['type'] = $field['type'] === 'NUMERIC' || $field['type'] === 'BIGINT' ? 'BIGSERIAL' : 'SERIAL';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific DROP TABLE string
|
||||
*/
|
||||
protected function _dropTable(string $table, bool $ifExists, bool $cascade): string
|
||||
{
|
||||
$sql = parent::_dropTable($table, $ifExists, $cascade);
|
||||
|
||||
if ($cascade === true) {
|
||||
$sql .= ' CASCADE';
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
111
system/Database/Postgre/PreparedQuery.php
Normal file
111
system/Database/Postgre/PreparedQuery.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use BadMethodCallException;
|
||||
use CodeIgniter\Database\BasePreparedQuery;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Prepared query for Postgre
|
||||
*/
|
||||
class PreparedQuery extends BasePreparedQuery
|
||||
{
|
||||
/**
|
||||
* Stores the name this query can be
|
||||
* used under by postgres. Only used internally.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The result resource from a successful
|
||||
* pg_exec. Or false.
|
||||
*
|
||||
* @var bool|Result
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* NOTE: This version is based on SQL code. Child classes should
|
||||
* override this method.
|
||||
*
|
||||
* @param array $options Passed to the connection's prepare statement.
|
||||
* Unused in the MySQLi driver.
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _prepare(string $sql, array $options = [])
|
||||
{
|
||||
$this->name = (string) random_int(1, 10000000000000000);
|
||||
|
||||
$sql = $this->parameterize($sql);
|
||||
|
||||
// Update the query object since the parameters are slightly different
|
||||
// than what was put in.
|
||||
$this->query->setQuery($sql);
|
||||
|
||||
if (! $this->statement = pg_prepare($this->db->connID, $this->name, $sql)) {
|
||||
$this->errorCode = 0;
|
||||
$this->errorString = pg_last_error($this->db->connID);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*/
|
||||
public function _execute(array $data): bool
|
||||
{
|
||||
if (! isset($this->statement)) {
|
||||
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
|
||||
}
|
||||
|
||||
$this->result = pg_execute($this->db->connID, $this->name, $data);
|
||||
|
||||
return (bool) $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result object for the prepared query.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _getResult()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the ? placeholders with $1, $2, etc parameters for use
|
||||
* within the prepared query.
|
||||
*/
|
||||
public function parameterize(string $sql): string
|
||||
{
|
||||
// Track our current value
|
||||
$count = 0;
|
||||
|
||||
return preg_replace_callback('/\?/', static function () use (&$count) {
|
||||
$count++;
|
||||
|
||||
return "\${$count}";
|
||||
}, $sql);
|
||||
}
|
||||
}
|
||||
128
system/Database/Postgre/Result.php
Normal file
128
system/Database/Postgre/Result.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Result for Postgre
|
||||
*/
|
||||
class Result extends BaseResult
|
||||
{
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getFieldCount(): int
|
||||
{
|
||||
return pg_num_fields($this->resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
public function getFieldNames(): array
|
||||
{
|
||||
$fieldNames = [];
|
||||
|
||||
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
|
||||
$fieldNames[] = pg_field_name($this->resultID, $i);
|
||||
}
|
||||
|
||||
return $fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
public function getFieldData(): array
|
||||
{
|
||||
$retVal = [];
|
||||
|
||||
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
|
||||
$retVal[$i] = new stdClass();
|
||||
$retVal[$i]->name = pg_field_name($this->resultID, $i);
|
||||
$retVal[$i]->type = pg_field_type_oid($this->resultID, $i);
|
||||
$retVal[$i]->type_name = pg_field_type($this->resultID, $i);
|
||||
$retVal[$i]->max_length = pg_field_size($this->resultID, $i);
|
||||
$retVal[$i]->length = $retVal[$i]->max_length;
|
||||
// $retVal[$i]->primary_key = (int)($fieldData[$i]->flags & 2);
|
||||
// $retVal[$i]->default = $fieldData[$i]->def;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*/
|
||||
public function freeResult()
|
||||
{
|
||||
if ($this->resultID !== false) {
|
||||
pg_free_result($this->resultID);
|
||||
$this->resultID = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function dataSeek(int $n = 0)
|
||||
{
|
||||
return pg_result_seek($this->resultID, $n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an array.
|
||||
*
|
||||
* Overridden by driver classes.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fetchAssoc()
|
||||
{
|
||||
return pg_fetch_assoc($this->resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an object.
|
||||
*
|
||||
* Overridden by child classes.
|
||||
*
|
||||
* @return bool|Entity|object
|
||||
*/
|
||||
protected function fetchObject(string $className = 'stdClass')
|
||||
{
|
||||
if (is_subclass_of($className, Entity::class)) {
|
||||
return empty($data = $this->fetchAssoc()) ? false : (new $className())->setAttributes($data);
|
||||
}
|
||||
|
||||
return pg_fetch_object($this->resultID, null, $className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of rows in the resultID (i.e., PostgreSQL query result resource)
|
||||
*/
|
||||
public function getNumRows(): int
|
||||
{
|
||||
if (! is_int($this->numRows)) {
|
||||
$this->numRows = pg_num_rows($this->resultID);
|
||||
}
|
||||
|
||||
return $this->numRows;
|
||||
}
|
||||
}
|
||||
45
system/Database/Postgre/Utils.php
Normal file
45
system/Database/Postgre/Utils.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\Postgre;
|
||||
|
||||
use CodeIgniter\Database\BaseUtils;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* Utils for Postgre
|
||||
*/
|
||||
class Utils extends BaseUtils
|
||||
{
|
||||
/**
|
||||
* List databases statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $listDatabases = 'SELECT datname FROM pg_database';
|
||||
|
||||
/**
|
||||
* OPTIMIZE TABLE statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $optimizeTable = 'REINDEX TABLE %s';
|
||||
|
||||
/**
|
||||
* Platform dependent version of the backup function.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _backup(?array $prefs = null)
|
||||
{
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
}
|
||||
54
system/Database/PreparedQueryInterface.php
Normal file
54
system/Database/PreparedQueryInterface.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
/**
|
||||
* Prepared query interface
|
||||
*/
|
||||
interface PreparedQueryInterface
|
||||
{
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*
|
||||
* @return ResultInterface
|
||||
*/
|
||||
public function execute(...$data);
|
||||
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function prepare(string $sql, array $options = []);
|
||||
|
||||
/**
|
||||
* Explicity closes the statement.
|
||||
*/
|
||||
public function close();
|
||||
|
||||
/**
|
||||
* Returns the SQL that has been prepared.
|
||||
*/
|
||||
public function getQueryString(): string;
|
||||
|
||||
/**
|
||||
* Returns the error code created while executing this statement.
|
||||
*/
|
||||
public function getErrorCode(): int;
|
||||
|
||||
/**
|
||||
* Returns the error message created while executing this statement.
|
||||
*/
|
||||
public function getErrorMessage(): string;
|
||||
}
|
||||
418
system/Database/Query.php
Normal file
418
system/Database/Query.php
Normal file
@@ -0,0 +1,418 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
/**
|
||||
* Query builder
|
||||
*/
|
||||
class Query implements QueryInterface
|
||||
{
|
||||
/**
|
||||
* The query string, as provided by the user.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $originalQueryString;
|
||||
|
||||
/**
|
||||
* The final query string after binding, etc.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $finalQueryString;
|
||||
|
||||
/**
|
||||
* The binds and their values used for binding.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $binds = [];
|
||||
|
||||
/**
|
||||
* Bind marker
|
||||
*
|
||||
* Character used to identify values in a prepared statement.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $bindMarker = '?';
|
||||
|
||||
/**
|
||||
* The start time in seconds with microseconds
|
||||
* for when this query was executed.
|
||||
*
|
||||
* @var float|string
|
||||
*/
|
||||
protected $startTime;
|
||||
|
||||
/**
|
||||
* The end time in seconds with microseconds
|
||||
* for when this query was executed.
|
||||
*
|
||||
* @var float
|
||||
*/
|
||||
protected $endTime;
|
||||
|
||||
/**
|
||||
* The error code, if any.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $errorCode;
|
||||
|
||||
/**
|
||||
* The error message, if any.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $errorString;
|
||||
|
||||
/**
|
||||
* Pointer to database connection.
|
||||
* Mainly for escaping features.
|
||||
*
|
||||
* @var ConnectionInterface
|
||||
*/
|
||||
public $db;
|
||||
|
||||
public function __construct(ConnectionInterface &$db)
|
||||
{
|
||||
$this->db = $db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the raw query string to use for this statement.
|
||||
*
|
||||
* @param mixed $binds
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setQuery(string $sql, $binds = null, bool $setEscape = true)
|
||||
{
|
||||
$this->originalQueryString = $sql;
|
||||
|
||||
if ($binds !== null) {
|
||||
if (! is_array($binds)) {
|
||||
$binds = [$binds];
|
||||
}
|
||||
|
||||
if ($setEscape) {
|
||||
array_walk($binds, static function (&$item) {
|
||||
$item = [
|
||||
$item,
|
||||
true,
|
||||
];
|
||||
});
|
||||
}
|
||||
$this->binds = $binds;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will store the variables to bind into the query later.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setBinds(array $binds, bool $setEscape = true)
|
||||
{
|
||||
if ($setEscape) {
|
||||
array_walk($binds, static function (&$item) {
|
||||
$item = [$item, true];
|
||||
});
|
||||
}
|
||||
|
||||
$this->binds = $binds;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the final, processed query string after binding, etal
|
||||
* has been performed.
|
||||
*/
|
||||
public function getQuery(): string
|
||||
{
|
||||
if (empty($this->finalQueryString)) {
|
||||
$this->finalQueryString = $this->originalQueryString;
|
||||
}
|
||||
|
||||
$this->compileBinds();
|
||||
|
||||
return $this->finalQueryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the execution time of the statement using microtime(true)
|
||||
* for it's start and end values. If no end value is present, will
|
||||
* use the current time to determine total duration.
|
||||
*
|
||||
* @param float $end
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setDuration(float $start, ?float $end = null)
|
||||
{
|
||||
$this->startTime = $start;
|
||||
|
||||
if ($end === null) {
|
||||
$end = microtime(true);
|
||||
}
|
||||
|
||||
$this->endTime = $end;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the start time in seconds with microseconds.
|
||||
*
|
||||
* @return float|string
|
||||
*/
|
||||
public function getStartTime(bool $returnRaw = false, int $decimals = 6)
|
||||
{
|
||||
if ($returnRaw) {
|
||||
return $this->startTime;
|
||||
}
|
||||
|
||||
return number_format($this->startTime, $decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the duration of this query during execution, or null if
|
||||
* the query has not been executed yet.
|
||||
*
|
||||
* @param int $decimals The accuracy of the returned time.
|
||||
*/
|
||||
public function getDuration(int $decimals = 6): string
|
||||
{
|
||||
return number_format(($this->endTime - $this->startTime), $decimals);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the error description that happened for this query.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setError(int $code, string $error)
|
||||
{
|
||||
$this->errorCode = $code;
|
||||
$this->errorString = $error;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports whether this statement created an error not.
|
||||
*/
|
||||
public function hasError(): bool
|
||||
{
|
||||
return ! empty($this->errorString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error code created while executing this statement.
|
||||
*/
|
||||
public function getErrorCode(): int
|
||||
{
|
||||
return $this->errorCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the error message created while executing this statement.
|
||||
*/
|
||||
public function getErrorMessage(): string
|
||||
{
|
||||
return $this->errorString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the statement is a write-type query or not.
|
||||
*/
|
||||
public function isWriteType(): bool
|
||||
{
|
||||
return $this->db->isWriteType($this->originalQueryString);
|
||||
}
|
||||
|
||||
/**
|
||||
* Swaps out one table prefix for a new one.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function swapPrefix(string $orig, string $swap)
|
||||
{
|
||||
$sql = empty($this->finalQueryString) ? $this->originalQueryString : $this->finalQueryString;
|
||||
|
||||
$this->finalQueryString = preg_replace('/(\W)' . $orig . '(\S+?)/', '\\1' . $swap . '\\2', $sql);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original SQL that was passed into the system.
|
||||
*/
|
||||
public function getOriginalQuery(): string
|
||||
{
|
||||
return $this->originalQueryString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes and inserts any binds into the finalQueryString object.
|
||||
*
|
||||
* @see https://regex101.com/r/EUEhay/5
|
||||
*/
|
||||
protected function compileBinds()
|
||||
{
|
||||
$sql = $this->finalQueryString;
|
||||
$binds = $this->binds;
|
||||
|
||||
if (empty($binds)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_int(array_key_first($binds))) {
|
||||
$bindCount = count($binds);
|
||||
$ml = strlen($this->bindMarker);
|
||||
|
||||
$this->finalQueryString = $this->matchSimpleBinds($sql, $binds, $bindCount, $ml);
|
||||
} else {
|
||||
// Reverse the binds so that duplicate named binds
|
||||
// will be processed prior to the original binds.
|
||||
$binds = array_reverse($binds);
|
||||
|
||||
$this->finalQueryString = $this->matchNamedBinds($sql, $binds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Match bindings
|
||||
*/
|
||||
protected function matchNamedBinds(string $sql, array $binds): string
|
||||
{
|
||||
$replacers = [];
|
||||
|
||||
foreach ($binds as $placeholder => $value) {
|
||||
// $value[1] contains the boolean whether should be escaped or not
|
||||
$escapedValue = $value[1] ? $this->db->escape($value[0]) : $value[0];
|
||||
|
||||
// In order to correctly handle backlashes in saved strings
|
||||
// we will need to preg_quote, so remove the wrapping escape characters
|
||||
// otherwise it will get escaped.
|
||||
if (is_array($value[0])) {
|
||||
$escapedValue = '(' . implode(',', $escapedValue) . ')';
|
||||
}
|
||||
|
||||
$replacers[":{$placeholder}:"] = $escapedValue;
|
||||
}
|
||||
|
||||
return strtr($sql, $replacers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Match bindings
|
||||
*/
|
||||
protected function matchSimpleBinds(string $sql, array $binds, int $bindCount, int $ml): string
|
||||
{
|
||||
if ($c = preg_match_all("/'[^']*'/", $sql, $matches)) {
|
||||
$c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', str_replace($matches[0], str_replace($this->bindMarker, str_repeat(' ', $ml), $matches[0]), $sql, $c), $matches, PREG_OFFSET_CAPTURE);
|
||||
|
||||
// Bind values' count must match the count of markers in the query
|
||||
if ($bindCount !== $c) {
|
||||
return $sql;
|
||||
}
|
||||
} elseif (($c = preg_match_all('/' . preg_quote($this->bindMarker, '/') . '/i', $sql, $matches, PREG_OFFSET_CAPTURE)) !== $bindCount) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
do {
|
||||
$c--;
|
||||
$escapedValue = $binds[$c][1] ? $this->db->escape($binds[$c][0]) : $binds[$c][0];
|
||||
|
||||
if (is_array($escapedValue)) {
|
||||
$escapedValue = '(' . implode(',', $escapedValue) . ')';
|
||||
}
|
||||
|
||||
$sql = substr_replace($sql, $escapedValue, $matches[0][$c][1], $ml);
|
||||
} while ($c !== 0);
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string to display in debug toolbar
|
||||
*/
|
||||
public function debugToolbarDisplay(): string
|
||||
{
|
||||
// Key words we want bolded
|
||||
static $highlight = [
|
||||
'AND',
|
||||
'AS',
|
||||
'ASC',
|
||||
'AVG',
|
||||
'BY',
|
||||
'COUNT',
|
||||
'DESC',
|
||||
'DISTINCT',
|
||||
'FROM',
|
||||
'GROUP',
|
||||
'HAVING',
|
||||
'IN',
|
||||
'INNER',
|
||||
'INSERT',
|
||||
'INTO',
|
||||
'IS',
|
||||
'JOIN',
|
||||
'LEFT',
|
||||
'LIKE',
|
||||
'LIMIT',
|
||||
'MAX',
|
||||
'MIN',
|
||||
'NOT',
|
||||
'NULL',
|
||||
'OFFSET',
|
||||
'ON',
|
||||
'OR',
|
||||
'ORDER',
|
||||
'RIGHT',
|
||||
'SELECT',
|
||||
'SUM',
|
||||
'UPDATE',
|
||||
'VALUES',
|
||||
'WHERE',
|
||||
];
|
||||
|
||||
if (empty($this->finalQueryString)) {
|
||||
$this->compileBinds(); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$sql = esc($this->finalQueryString);
|
||||
|
||||
/**
|
||||
* @see https://stackoverflow.com/a/20767160
|
||||
* @see https://regex101.com/r/hUlrGN/4
|
||||
*/
|
||||
$search = '/\b(?:' . implode('|', $highlight) . ')\b(?![^(')]*'(?:(?:[^(')]*'){2})*[^(')]*$)/';
|
||||
|
||||
return preg_replace_callback($search, static function ($matches) {
|
||||
return '<strong>' . str_replace(' ', ' ', $matches[0]) . '</strong>';
|
||||
}, $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return text representation of the query
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getQuery();
|
||||
}
|
||||
}
|
||||
87
system/Database/QueryInterface.php
Normal file
87
system/Database/QueryInterface.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
/**
|
||||
* Interface QueryInterface
|
||||
*
|
||||
* Represents a single statement that can be executed against the database.
|
||||
* Statements are platform-specific and can handle binding of binds.
|
||||
*/
|
||||
interface QueryInterface
|
||||
{
|
||||
/**
|
||||
* Sets the raw query string to use for this statement.
|
||||
*
|
||||
* @param mixed $binds
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function setQuery(string $sql, $binds = null, bool $setEscape = true);
|
||||
|
||||
/**
|
||||
* Returns the final, processed query string after binding, etal
|
||||
* has been performed.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getQuery();
|
||||
|
||||
/**
|
||||
* Records the execution time of the statement using microtime(true)
|
||||
* for it's start and end values. If no end value is present, will
|
||||
* use the current time to determine total duration.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function setDuration(float $start, ?float $end = null);
|
||||
|
||||
/**
|
||||
* Returns the duration of this query during execution, or null if
|
||||
* the query has not been executed yet.
|
||||
*
|
||||
* @param int $decimals The accuracy of the returned time.
|
||||
*/
|
||||
public function getDuration(int $decimals = 6): string;
|
||||
|
||||
/**
|
||||
* Stores the error description that happened for this query.
|
||||
*/
|
||||
public function setError(int $code, string $error);
|
||||
|
||||
/**
|
||||
* Reports whether this statement created an error not.
|
||||
*/
|
||||
public function hasError(): bool;
|
||||
|
||||
/**
|
||||
* Returns the error code created while executing this statement.
|
||||
*/
|
||||
public function getErrorCode(): int;
|
||||
|
||||
/**
|
||||
* Returns the error message created while executing this statement.
|
||||
*/
|
||||
public function getErrorMessage(): string;
|
||||
|
||||
/**
|
||||
* Determines if the statement is a write-type query or not.
|
||||
*/
|
||||
public function isWriteType(): bool;
|
||||
|
||||
/**
|
||||
* Swaps out one table prefix for a new one.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function swapPrefix(string $orig, string $swap);
|
||||
}
|
||||
164
system/Database/ResultInterface.php
Normal file
164
system/Database/ResultInterface.php
Normal file
@@ -0,0 +1,164 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
/**
|
||||
* Interface ResultInterface
|
||||
*/
|
||||
interface ResultInterface
|
||||
{
|
||||
/**
|
||||
* Retrieve the results of the query. Typically an array of
|
||||
* individual data rows, which can be either an 'array', an
|
||||
* 'object', or a custom class name.
|
||||
*
|
||||
* @param string $type The row type. Either 'array', 'object', or a class name to use
|
||||
*/
|
||||
public function getResult(string $type = 'object'): array;
|
||||
|
||||
/**
|
||||
* Returns the results as an array of custom objects.
|
||||
*
|
||||
* @param string $className The name of the class to use.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCustomResultObject(string $className);
|
||||
|
||||
/**
|
||||
* Returns the results as an array of arrays.
|
||||
*
|
||||
* If no results, an empty array is returned.
|
||||
*/
|
||||
public function getResultArray(): array;
|
||||
|
||||
/**
|
||||
* Returns the results as an array of objects.
|
||||
*
|
||||
* If no results, an empty array is returned.
|
||||
*/
|
||||
public function getResultObject(): array;
|
||||
|
||||
/**
|
||||
* Wrapper object to return a row as either an array, an object, or
|
||||
* a custom class.
|
||||
*
|
||||
* If row doesn't exist, returns null.
|
||||
*
|
||||
* @param mixed $n The index of the results to return
|
||||
* @param string $type The type of result object. 'array', 'object' or class name.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRow($n = 0, string $type = 'object');
|
||||
|
||||
/**
|
||||
* Returns a row as a custom class instance.
|
||||
*
|
||||
* If row doesn't exists, returns null.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getCustomRowObject(int $n, string $className);
|
||||
|
||||
/**
|
||||
* Returns a single row from the results as an array.
|
||||
*
|
||||
* If row doesn't exist, returns null.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRowArray(int $n = 0);
|
||||
|
||||
/**
|
||||
* Returns a single row from the results as an object.
|
||||
*
|
||||
* If row doesn't exist, returns null.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRowObject(int $n = 0);
|
||||
|
||||
/**
|
||||
* Assigns an item into a particular column slot.
|
||||
*
|
||||
* @param string $key
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function setRow($key, $value = null);
|
||||
|
||||
/**
|
||||
* Returns the "first" row of the current results.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getFirstRow(string $type = 'object');
|
||||
|
||||
/**
|
||||
* Returns the "last" row of the current results.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLastRow(string $type = 'object');
|
||||
|
||||
/**
|
||||
* Returns the "next" row of the current results.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getNextRow(string $type = 'object');
|
||||
|
||||
/**
|
||||
* Returns the "previous" row of the current results.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPreviousRow(string $type = 'object');
|
||||
|
||||
/**
|
||||
* Returns an unbuffered row and move the pointer to the next row.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getUnbufferedRow(string $type = 'object');
|
||||
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getFieldCount(): int;
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
public function getFieldNames(): array;
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
public function getFieldData(): array;
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*/
|
||||
public function freeResult();
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function dataSeek(int $n = 0);
|
||||
}
|
||||
625
system/Database/SQLSRV/Builder.php
Normal file
625
system/Database/SQLSRV/Builder.php
Normal file
@@ -0,0 +1,625 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLSRV;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\Exceptions\DataException;
|
||||
use CodeIgniter\Database\ResultInterface;
|
||||
|
||||
/**
|
||||
* Builder for SQLSRV
|
||||
*
|
||||
* @todo auto check for TextCastToInt
|
||||
* @todo auto check for InsertIndexValue
|
||||
* @todo replace: delete index entries before insert
|
||||
*/
|
||||
class Builder extends BaseBuilder
|
||||
{
|
||||
/**
|
||||
* ORDER BY random keyword
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $randomKeyword = [
|
||||
'NEWID()',
|
||||
'RAND(%d)',
|
||||
];
|
||||
|
||||
/**
|
||||
* Quoted identifier flag
|
||||
*
|
||||
* Whether to use SQL-92 standard quoted identifier
|
||||
* (double quotes) or brackets for identifier escaping.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_quoted_identifier = true;
|
||||
|
||||
/**
|
||||
* Handle increment/decrement on text
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $castTextToInt = true;
|
||||
|
||||
/**
|
||||
* Handle IDENTITY_INSERT property/
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $keyPermission = false;
|
||||
|
||||
/**
|
||||
* Groups tables in FROM clauses if needed, so there is no confusion
|
||||
* about operator precedence.
|
||||
*/
|
||||
protected function _fromTables(): string
|
||||
{
|
||||
$from = [];
|
||||
|
||||
foreach ($this->QBFrom as $value) {
|
||||
$from[] = $this->getFullName($value);
|
||||
}
|
||||
|
||||
return implode(', ', $from);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific truncate string from the supplied data
|
||||
*
|
||||
* If the database does not support the truncate() command,
|
||||
* then this method maps to 'DELETE FROM table'
|
||||
*/
|
||||
protected function _truncate(string $table): string
|
||||
{
|
||||
return 'TRUNCATE TABLE ' . $this->getFullName($table);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the JOIN portion of the query
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function join(string $table, string $cond, string $type = '', ?bool $escape = null)
|
||||
{
|
||||
if ($type !== '') {
|
||||
$type = strtoupper(trim($type));
|
||||
|
||||
if (! in_array($type, $this->joinTypes, true)) {
|
||||
$type = '';
|
||||
} else {
|
||||
$type .= ' ';
|
||||
}
|
||||
}
|
||||
|
||||
// Extract any aliases that might exist. We use this information
|
||||
// in the protectIdentifiers to know whether to add a table prefix
|
||||
$this->trackAliases($table);
|
||||
|
||||
if (! is_bool($escape)) {
|
||||
$escape = $this->db->protectIdentifiers;
|
||||
}
|
||||
|
||||
if (! $this->hasOperator($cond)) {
|
||||
$cond = ' USING (' . ($escape ? $this->db->escapeIdentifiers($cond) : $cond) . ')';
|
||||
} elseif ($escape === false) {
|
||||
$cond = ' ON ' . $cond;
|
||||
} else {
|
||||
// Split multiple conditions
|
||||
if (preg_match_all('/\sAND\s|\sOR\s/i', $cond, $joints, PREG_OFFSET_CAPTURE)) {
|
||||
$conditions = [];
|
||||
$joints = $joints[0];
|
||||
array_unshift($joints, ['', 0]);
|
||||
|
||||
for ($i = count($joints) - 1, $pos = strlen($cond); $i >= 0; $i--) {
|
||||
$joints[$i][1] += strlen($joints[$i][0]); // offset
|
||||
$conditions[$i] = substr($cond, $joints[$i][1], $pos - $joints[$i][1]);
|
||||
$pos = $joints[$i][1] - strlen($joints[$i][0]);
|
||||
$joints[$i] = $joints[$i][0];
|
||||
}
|
||||
|
||||
ksort($conditions);
|
||||
} else {
|
||||
$conditions = [$cond];
|
||||
$joints = [''];
|
||||
}
|
||||
|
||||
$cond = ' ON ';
|
||||
|
||||
foreach ($conditions as $i => $condition) {
|
||||
$operator = $this->getOperator($condition);
|
||||
|
||||
$cond .= $joints[$i];
|
||||
$cond .= preg_match('/(\(*)?([\[\]\w\.\'-]+)' . preg_quote($operator, '/') . '(.*)/i', $condition, $match) ? $match[1] . $this->db->protectIdentifiers($match[2]) . $operator . $this->db->protectIdentifiers($match[3]) : $condition;
|
||||
}
|
||||
}
|
||||
|
||||
// Do we want to escape the table name?
|
||||
if ($escape === true) {
|
||||
$table = $this->db->protectIdentifiers($table, true, null, false);
|
||||
}
|
||||
|
||||
// Assemble the JOIN statement
|
||||
$this->QBJoin[] = $type . 'JOIN ' . $this->getFullName($table) . $cond;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific insert string from the supplied data
|
||||
*
|
||||
* @todo implement check for this instead static $insertKeyPermission
|
||||
*/
|
||||
protected function _insert(string $table, array $keys, array $unescapedKeys): string
|
||||
{
|
||||
$fullTableName = $this->getFullName($table);
|
||||
|
||||
// insert statement
|
||||
$statement = 'INSERT INTO ' . $fullTableName . ' (' . implode(',', $keys) . ') VALUES (' . implode(', ', $unescapedKeys) . ')';
|
||||
|
||||
return $this->keyPermission ? $this->addIdentity($fullTableName, $statement) : $statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert batch statement
|
||||
*
|
||||
* Generates a platform-specific insert string from the supplied data.
|
||||
*/
|
||||
protected function _insertBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
return 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $this->getFullName($table) . ' (' . implode(', ', $keys) . ') VALUES ' . implode(', ', $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific update string from the supplied data
|
||||
*/
|
||||
protected function _update(string $table, array $values): string
|
||||
{
|
||||
$valstr = [];
|
||||
|
||||
foreach ($values as $key => $val) {
|
||||
$valstr[] = $key . ' = ' . $val;
|
||||
}
|
||||
|
||||
$fullTableName = $this->getFullName($table);
|
||||
|
||||
$statement = sprintf('UPDATE %s%s SET ', empty($this->QBLimit) ? '' : 'TOP(' . $this->QBLimit . ') ', $fullTableName);
|
||||
|
||||
$statement .= implode(', ', $valstr)
|
||||
. $this->compileWhereHaving('QBWhere')
|
||||
. $this->compileOrderBy();
|
||||
|
||||
return $this->keyPermission ? $this->addIdentity($fullTableName, $statement) : $statement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update_Batch statement
|
||||
*
|
||||
* Generates a platform-specific batch update string from the supplied data
|
||||
*/
|
||||
protected function _updateBatch(string $table, array $values, string $index): string
|
||||
{
|
||||
$ids = [];
|
||||
$final = [];
|
||||
|
||||
foreach ($values as $val) {
|
||||
$ids[] = $val[$index];
|
||||
|
||||
foreach (array_keys($val) as $field) {
|
||||
if ($field !== $index) {
|
||||
$final[$field][] = 'WHEN ' . $index . ' = ' . $val[$index] . ' THEN ' . $val[$field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cases = '';
|
||||
|
||||
foreach ($final as $k => $v) {
|
||||
$cases .= $k . " = CASE \n"
|
||||
. implode("\n", $v) . "\n"
|
||||
. 'ELSE ' . $k . ' END, ';
|
||||
}
|
||||
|
||||
$this->where($index . ' IN(' . implode(',', $ids) . ')', null, false);
|
||||
|
||||
return 'UPDATE ' . $this->compileIgnore('update') . ' ' . $this->getFullName($table) . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere');
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments a numeric column by the specified value.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function increment(string $column, int $value = 1)
|
||||
{
|
||||
$column = $this->db->protectIdentifiers($column);
|
||||
|
||||
if ($this->castTextToInt) {
|
||||
$values = [$column => "CONVERT(VARCHAR(MAX),CONVERT(INT,CONVERT(VARCHAR(MAX), {$column})) + {$value})"];
|
||||
} else {
|
||||
$values = [$column => "{$column} + {$value}"];
|
||||
}
|
||||
|
||||
$sql = $this->_update($this->QBFrom[0], $values);
|
||||
|
||||
return $this->db->query($sql, $this->binds, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrements a numeric column by the specified value.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function decrement(string $column, int $value = 1)
|
||||
{
|
||||
$column = $this->db->protectIdentifiers($column);
|
||||
|
||||
if ($this->castTextToInt) {
|
||||
$values = [$column => "CONVERT(VARCHAR(MAX),CONVERT(INT,CONVERT(VARCHAR(MAX), {$column})) - {$value})"];
|
||||
} else {
|
||||
$values = [$column => "{$column} + {$value}"];
|
||||
}
|
||||
|
||||
$sql = $this->_update($this->QBFrom[0], $values);
|
||||
|
||||
return $this->db->query($sql, $this->binds, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full name of the table
|
||||
*/
|
||||
private function getFullName(string $table): string
|
||||
{
|
||||
$alias = '';
|
||||
|
||||
if (strpos($table, ' ') !== false) {
|
||||
$alias = explode(' ', $table);
|
||||
$table = array_shift($alias);
|
||||
$alias = ' ' . implode(' ', $alias);
|
||||
}
|
||||
|
||||
if ($this->db->escapeChar === '"') {
|
||||
return '"' . $this->db->getDatabase() . '"."' . $this->db->schema . '"."' . str_replace('"', '', $table) . '"' . $alias;
|
||||
}
|
||||
|
||||
return '[' . $this->db->getDatabase() . '].[' . $this->db->schema . '].[' . str_replace('"', '', $table) . ']' . str_replace('"', '', $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add permision statements for index value inserts
|
||||
*/
|
||||
private function addIdentity(string $fullTable, string $insert): string
|
||||
{
|
||||
return 'SET IDENTITY_INSERT ' . $fullTable . " ON\n" . $insert . "\nSET IDENTITY_INSERT " . $fullTable . ' OFF';
|
||||
}
|
||||
|
||||
/**
|
||||
* Local implementation of limit
|
||||
*/
|
||||
protected function _limit(string $sql, bool $offsetIgnore = false): string
|
||||
{
|
||||
if (empty($this->QBOrderBy)) {
|
||||
$sql .= ' ORDER BY (SELECT NULL) ';
|
||||
}
|
||||
|
||||
if ($offsetIgnore) {
|
||||
$sql .= ' OFFSET 0 ';
|
||||
} else {
|
||||
$sql .= is_int($this->QBOffset) ? ' OFFSET ' . $this->QBOffset : ' OFFSET 0 ';
|
||||
}
|
||||
|
||||
return $sql . ' ROWS FETCH NEXT ' . $this->QBLimit . ' ROWS ONLY ';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles a replace into string and runs the query
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function replace(?array $set = null)
|
||||
{
|
||||
if ($set !== null) {
|
||||
$this->set($set);
|
||||
}
|
||||
|
||||
if (empty($this->QBSet)) {
|
||||
if (CI_DEBUG) {
|
||||
throw new DatabaseException('You must use the "set" method to update an entry.');
|
||||
}
|
||||
|
||||
return false; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$table = $this->QBFrom[0];
|
||||
|
||||
$sql = $this->_replace($table, array_keys($this->QBSet), array_values($this->QBSet));
|
||||
|
||||
$this->resetWrite();
|
||||
|
||||
if ($this->testMode) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->getFullName($table) . ' ON');
|
||||
|
||||
$result = $this->db->query($sql, $this->binds, false);
|
||||
$this->db->simpleQuery('SET IDENTITY_INSERT ' . $this->getFullName($table) . ' OFF');
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific replace string from the supplied data
|
||||
* on match delete and insert
|
||||
*/
|
||||
protected function _replace(string $table, array $keys, array $values): string
|
||||
{
|
||||
// check whether the existing keys are part of the primary key.
|
||||
// if so then use them for the "ON" part and exclude them from the $values and $keys
|
||||
$pKeys = $this->db->getIndexData($table);
|
||||
$keyFields = [];
|
||||
|
||||
foreach ($pKeys as $key) {
|
||||
if ($key->type === 'PRIMARY') {
|
||||
$keyFields = array_merge($keyFields, $key->fields);
|
||||
}
|
||||
|
||||
if ($key->type === 'UNIQUE') {
|
||||
$keyFields = array_merge($keyFields, $key->fields);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the unique field names
|
||||
$escKeyFields = array_map(function (string $field): string {
|
||||
return $this->db->protectIdentifiers($field);
|
||||
}, array_values(array_unique($keyFields)));
|
||||
|
||||
// Get the binds
|
||||
$binds = $this->binds;
|
||||
array_walk($binds, static function (&$item) {
|
||||
$item = $item[0];
|
||||
});
|
||||
|
||||
// Get the common field and values from the keys data and index fields
|
||||
$common = array_intersect($keys, $escKeyFields);
|
||||
$bingo = [];
|
||||
|
||||
foreach ($common as $v) {
|
||||
$k = array_search($v, $keys, true);
|
||||
|
||||
$bingo[$keys[$k]] = $binds[trim($values[$k], ':')];
|
||||
}
|
||||
|
||||
// Querying existing data
|
||||
$builder = $this->db->table($table);
|
||||
|
||||
foreach ($bingo as $k => $v) {
|
||||
$builder->where($k, $v);
|
||||
}
|
||||
|
||||
$q = $builder->get()->getResult();
|
||||
|
||||
// Delete entries if we find them
|
||||
if ($q !== []) {
|
||||
$delete = $this->db->table($table);
|
||||
|
||||
foreach ($bingo as $k => $v) {
|
||||
$delete->where($k, $v);
|
||||
}
|
||||
|
||||
$delete->delete();
|
||||
}
|
||||
|
||||
return sprintf('INSERT INTO %s (%s) VALUES (%s);', $this->getFullName($table), implode(',', $keys), implode(',', $values));
|
||||
}
|
||||
|
||||
/**
|
||||
* SELECT [MAX|MIN|AVG|SUM|COUNT]()
|
||||
*
|
||||
* Handle float return value
|
||||
*
|
||||
* @return BaseBuilder
|
||||
*/
|
||||
protected function maxMinAvgSum(string $select = '', string $alias = '', string $type = 'MAX')
|
||||
{
|
||||
// int functions can be handled by parent
|
||||
if ($type !== 'AVG') {
|
||||
return parent::maxMinAvgSum($select, $alias, $type);
|
||||
}
|
||||
|
||||
if ($select === '') {
|
||||
throw DataException::forEmptyInputGiven('Select');
|
||||
}
|
||||
|
||||
if (strpos($select, ',') !== false) {
|
||||
throw DataException::forInvalidArgument('Column name not separated by comma');
|
||||
}
|
||||
|
||||
if ($alias === '') {
|
||||
$alias = $this->createAliasFromTable(trim($select));
|
||||
}
|
||||
|
||||
$sql = $type . '( CAST( ' . $this->db->protectIdentifiers(trim($select)) . ' AS FLOAT ) ) AS ' . $this->db->escapeIdentifiers(trim($alias));
|
||||
|
||||
$this->QBSelect[] = $sql;
|
||||
$this->QBNoEscape[] = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Count All" query
|
||||
*
|
||||
* Generates a platform-specific query string that counts all records in
|
||||
* the particular table
|
||||
*
|
||||
* @param bool $reset Are we want to clear query builder values?
|
||||
*
|
||||
* @return int|string when $test = true
|
||||
*/
|
||||
public function countAll(bool $reset = true)
|
||||
{
|
||||
$table = $this->QBFrom[0];
|
||||
|
||||
$sql = $this->countString . $this->db->escapeIdentifiers('numrows') . ' FROM ' . $this->getFullName($table);
|
||||
|
||||
if ($this->testMode) {
|
||||
return $sql;
|
||||
}
|
||||
|
||||
$query = $this->db->query($sql, null, false);
|
||||
if (empty($query->getResult())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$query = $query->getRow();
|
||||
|
||||
if ($reset === true) {
|
||||
$this->resetSelect();
|
||||
}
|
||||
|
||||
return (int) $query->numrows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete statement
|
||||
*/
|
||||
protected function _delete(string $table): string
|
||||
{
|
||||
return 'DELETE' . (empty($this->QBLimit) ? '' : ' TOP (' . $this->QBLimit . ') ') . ' FROM ' . $this->getFullName($table) . $this->compileWhereHaving('QBWhere');
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles a delete string and runs the query
|
||||
*
|
||||
* @param mixed $where
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete($where = '', ?int $limit = null, bool $resetData = true)
|
||||
{
|
||||
$table = $this->db->protectIdentifiers($this->QBFrom[0], true, null, false);
|
||||
|
||||
if ($where !== '') {
|
||||
$this->where($where);
|
||||
}
|
||||
|
||||
if (empty($this->QBWhere)) {
|
||||
if (CI_DEBUG) {
|
||||
throw new DatabaseException('Deletes are not allowed unless they contain a "where" or "like" clause.');
|
||||
}
|
||||
|
||||
return false; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
if (! empty($limit)) {
|
||||
$this->QBLimit = $limit;
|
||||
}
|
||||
|
||||
$sql = $this->_delete($table);
|
||||
|
||||
if ($resetData) {
|
||||
$this->resetWrite();
|
||||
}
|
||||
|
||||
return $this->testMode ? $sql : $this->db->query($sql, $this->binds, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compile the SELECT statement
|
||||
*
|
||||
* Generates a query string based on which functions were used.
|
||||
*
|
||||
* @param bool $selectOverride
|
||||
*/
|
||||
protected function compileSelect($selectOverride = false): string
|
||||
{
|
||||
// Write the "select" portion of the query
|
||||
if ($selectOverride !== false) {
|
||||
$sql = $selectOverride;
|
||||
} else {
|
||||
$sql = (! $this->QBDistinct) ? 'SELECT ' : 'SELECT DISTINCT ';
|
||||
|
||||
// SQL Server can't work with select * if group by is specified
|
||||
if (empty($this->QBSelect) && ! empty($this->QBGroupBy) && is_array($this->QBGroupBy)) {
|
||||
foreach ($this->QBGroupBy as $field) {
|
||||
$this->QBSelect[] = is_array($field) ? $field['field'] : $field;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->QBSelect)) {
|
||||
$sql .= '*';
|
||||
} else {
|
||||
// Cycle through the "select" portion of the query and prep each column name.
|
||||
// The reason we protect identifiers here rather than in the select() function
|
||||
// is because until the user calls the from() function we don't know if there are aliases
|
||||
foreach ($this->QBSelect as $key => $val) {
|
||||
$noEscape = $this->QBNoEscape[$key] ?? null;
|
||||
$this->QBSelect[$key] = $this->db->protectIdentifiers($val, false, $noEscape);
|
||||
}
|
||||
|
||||
$sql .= implode(', ', $this->QBSelect);
|
||||
}
|
||||
}
|
||||
|
||||
// Write the "FROM" portion of the query
|
||||
if (! empty($this->QBFrom)) {
|
||||
$sql .= "\nFROM " . $this->_fromTables();
|
||||
}
|
||||
|
||||
// Write the "JOIN" portion of the query
|
||||
if (! empty($this->QBJoin)) {
|
||||
$sql .= "\n" . implode("\n", $this->QBJoin);
|
||||
}
|
||||
|
||||
$sql .= $this->compileWhereHaving('QBWhere')
|
||||
. $this->compileGroupBy()
|
||||
. $this->compileWhereHaving('QBHaving')
|
||||
. $this->compileOrderBy(); // ORDER BY
|
||||
|
||||
// LIMIT
|
||||
if ($this->QBLimit) {
|
||||
$sql = $this->_limit($sql . "\n");
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles the select statement based on the other functions called
|
||||
* and runs the query
|
||||
*
|
||||
* @return ResultInterface
|
||||
*/
|
||||
public function get(?int $limit = null, int $offset = 0, bool $reset = true)
|
||||
{
|
||||
if ($limit !== null) {
|
||||
$this->limit($limit, $offset);
|
||||
}
|
||||
|
||||
$result = $this->testMode ? $this->getCompiledSelect($reset) : $this->db->query($this->compileSelect(), $this->binds, false);
|
||||
|
||||
if ($reset) {
|
||||
$this->resetSelect();
|
||||
|
||||
// Clear our binds so we don't eat up memory
|
||||
$this->binds = [];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
537
system/Database/SQLSRV/Connection.php
Normal file
537
system/Database/SQLSRV/Connection.php
Normal file
@@ -0,0 +1,537 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLSRV;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use Exception;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Connection for SQLSRV
|
||||
*/
|
||||
class Connection extends BaseConnection
|
||||
{
|
||||
/**
|
||||
* Database driver
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $DBDriver = 'SQLSRV';
|
||||
|
||||
/**
|
||||
* Database name
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $database;
|
||||
|
||||
/**
|
||||
* Scrollable flag
|
||||
*
|
||||
* Determines what cursor type to use when executing queries.
|
||||
*
|
||||
* FALSE or SQLSRV_CURSOR_FORWARD would increase performance,
|
||||
* but would disable num_rows() (and possibly insert_id())
|
||||
*
|
||||
* @var mixed
|
||||
*/
|
||||
public $scrollable;
|
||||
|
||||
/**
|
||||
* Identifier escape character
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $escapeChar = '"';
|
||||
|
||||
/**
|
||||
* Database schema
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $schema = 'dbo';
|
||||
|
||||
/**
|
||||
* Quoted identifier flag
|
||||
*
|
||||
* Whether to use SQL-92 standard quoted identifier
|
||||
* (double quotes) or brackets for identifier escaping.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $_quoted_identifier = true;
|
||||
|
||||
/**
|
||||
* List of reserved identifiers
|
||||
*
|
||||
* Identifiers that must NOT be escaped.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $_reserved_identifiers = ['*'];
|
||||
|
||||
/**
|
||||
* Class constructor
|
||||
*/
|
||||
public function __construct(array $params)
|
||||
{
|
||||
parent::__construct($params);
|
||||
|
||||
// This is only supported as of SQLSRV 3.0
|
||||
if ($this->scrollable === null) {
|
||||
$this->scrollable = defined('SQLSRV_CURSOR_CLIENT_BUFFERED') ? SQLSRV_CURSOR_CLIENT_BUFFERED : false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database.
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function connect(bool $persistent = false)
|
||||
{
|
||||
$charset = in_array(strtolower($this->charset), ['utf-8', 'utf8'], true) ? 'UTF-8' : SQLSRV_ENC_CHAR;
|
||||
|
||||
$connection = [
|
||||
'UID' => empty($this->username) ? '' : $this->username,
|
||||
'PWD' => empty($this->password) ? '' : $this->password,
|
||||
'Database' => $this->database,
|
||||
'ConnectionPooling' => $persistent ? 1 : 0,
|
||||
'CharacterSet' => $charset,
|
||||
'Encrypt' => $this->encrypt === true ? 1 : 0,
|
||||
'ReturnDatesAsStrings' => 1,
|
||||
];
|
||||
|
||||
// If the username and password are both empty, assume this is a
|
||||
// 'Windows Authentication Mode' connection.
|
||||
if (empty($connection['UID']) && empty($connection['PWD'])) {
|
||||
unset($connection['UID'], $connection['PWD']);
|
||||
}
|
||||
|
||||
sqlsrv_configure('WarningsReturnAsErrors', 0);
|
||||
$this->connID = sqlsrv_connect($this->hostname, $connection);
|
||||
|
||||
if ($this->connID !== false) {
|
||||
// Determine how identifiers are escaped
|
||||
$query = $this->query('SELECT CASE WHEN (@@OPTIONS | 256) = @@OPTIONS THEN 1 ELSE 0 END AS qi');
|
||||
$query = $query->getResultObject();
|
||||
|
||||
$this->_quoted_identifier = empty($query) ? false : (bool) $query[0]->qi;
|
||||
$this->escapeChar = ($this->_quoted_identifier) ? '"' : ['[', ']'];
|
||||
|
||||
return $this->connID;
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
|
||||
foreach (sqlsrv_errors(SQLSRV_ERR_ERRORS) as $error) {
|
||||
$errors[] = preg_replace('/(\[.+\]\[.+\](?:\[.+\])?)(.+)/', '$2', $error['message']);
|
||||
}
|
||||
|
||||
throw new DatabaseException(implode("\n", $errors));
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep or establish the connection if no queries have been sent for
|
||||
* a length of time exceeding the server's idle timeout.
|
||||
*/
|
||||
public function reconnect()
|
||||
{
|
||||
$this->close();
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection.
|
||||
*/
|
||||
protected function _close()
|
||||
{
|
||||
sqlsrv_close($this->connID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-dependant string escape
|
||||
*/
|
||||
protected function _escapeString(string $str): string
|
||||
{
|
||||
return str_replace("'", "''", remove_invisible_characters($str, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert ID
|
||||
*/
|
||||
public function insertID(): int
|
||||
{
|
||||
return $this->query('SELECT SCOPE_IDENTITY() AS insert_id')->getRow()->insert_id ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the SQL for listing tables in a platform-dependent manner.
|
||||
*/
|
||||
protected function _listTables(bool $prefixLimit = false): string
|
||||
{
|
||||
$sql = 'SELECT [TABLE_NAME] AS "name"'
|
||||
. ' FROM [INFORMATION_SCHEMA].[TABLES] '
|
||||
. ' WHERE '
|
||||
. " [TABLE_SCHEMA] = '" . $this->schema . "' ";
|
||||
|
||||
if ($prefixLimit === true && $this->DBPrefix !== '') {
|
||||
$sql .= " AND [TABLE_NAME] LIKE '" . $this->escapeLikeString($this->DBPrefix) . "%' "
|
||||
. sprintf($this->likeEscapeStr, $this->likeEscapeChar);
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific query string so that the column names can be fetched.
|
||||
*/
|
||||
protected function _listColumns(string $table = ''): string
|
||||
{
|
||||
return 'SELECT [COLUMN_NAME] '
|
||||
. ' FROM [INFORMATION_SCHEMA].[COLUMNS]'
|
||||
. ' WHERE [TABLE_NAME] = ' . $this->escape($this->DBPrefix . $table)
|
||||
. ' AND [TABLE_SCHEMA] = ' . $this->escape($this->schema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with index data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _indexData(string $table): array
|
||||
{
|
||||
$sql = 'EXEC sp_helpindex ' . $this->escape($this->schema . '.' . $table);
|
||||
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetIndexData'));
|
||||
}
|
||||
$query = $query->getResultObject();
|
||||
|
||||
$retVal = [];
|
||||
|
||||
foreach ($query as $row) {
|
||||
$obj = new stdClass();
|
||||
$obj->name = $row->index_name;
|
||||
|
||||
$_fields = explode(',', trim($row->index_keys));
|
||||
$obj->fields = array_map(static function ($v) {
|
||||
return trim($v);
|
||||
}, $_fields);
|
||||
|
||||
if (strpos($row->index_description, 'primary key located on') !== false) {
|
||||
$obj->type = 'PRIMARY';
|
||||
} else {
|
||||
$obj->type = (strpos($row->index_description, 'nonclustered, unique') !== false) ? 'UNIQUE' : 'INDEX';
|
||||
}
|
||||
|
||||
$retVal[$obj->name] = $obj;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with Foreign key data
|
||||
* referenced_object_id parent_object_id
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _foreignKeyData(string $table): array
|
||||
{
|
||||
$sql = 'SELECT '
|
||||
. 'f.name as constraint_name, '
|
||||
. 'OBJECT_NAME (f.parent_object_id) as table_name, '
|
||||
. 'COL_NAME(fc.parent_object_id,fc.parent_column_id) column_name, '
|
||||
. 'OBJECT_NAME(f.referenced_object_id) foreign_table_name, '
|
||||
. 'COL_NAME(fc.referenced_object_id,fc.referenced_column_id) foreign_column_name '
|
||||
. 'FROM '
|
||||
. 'sys.foreign_keys AS f '
|
||||
. 'INNER JOIN '
|
||||
. 'sys.foreign_key_columns AS fc '
|
||||
. 'ON f.OBJECT_ID = fc.constraint_object_id '
|
||||
. 'INNER JOIN '
|
||||
. 'sys.tables t '
|
||||
. 'ON t.OBJECT_ID = fc.referenced_object_id '
|
||||
. 'WHERE '
|
||||
. 'OBJECT_NAME (f.parent_object_id) = ' . $this->escape($table);
|
||||
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetForeignKeyData'));
|
||||
}
|
||||
|
||||
$query = $query->getResultObject();
|
||||
$retVal = [];
|
||||
|
||||
foreach ($query as $row) {
|
||||
$obj = new stdClass();
|
||||
|
||||
$obj->constraint_name = $row->constraint_name;
|
||||
$obj->table_name = $row->table_name;
|
||||
$obj->column_name = $row->column_name;
|
||||
$obj->foreign_table_name = $row->foreign_table_name;
|
||||
$obj->foreign_column_name = $row->foreign_column_name;
|
||||
|
||||
$retVal[] = $obj;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables foreign key checks temporarily.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _disableForeignKeyChecks()
|
||||
{
|
||||
return 'EXEC sp_MSforeachtable "ALTER TABLE ? NOCHECK CONSTRAINT ALL"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables foreign key checks temporarily.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _enableForeignKeyChecks()
|
||||
{
|
||||
return 'EXEC sp_MSforeachtable "ALTER TABLE ? WITH CHECK CHECK CONSTRAINT ALL"';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with field data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _fieldData(string $table): array
|
||||
{
|
||||
$sql = 'SELECT COLUMN_NAME, DATA_TYPE, CHARACTER_MAXIMUM_LENGTH, NUMERIC_PRECISION, COLUMN_DEFAULT
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_NAME= ' . $this->escape(($table));
|
||||
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetFieldData'));
|
||||
}
|
||||
|
||||
$query = $query->getResultObject();
|
||||
$retVal = [];
|
||||
|
||||
for ($i = 0, $c = count($query); $i < $c; $i++) {
|
||||
$retVal[$i] = new stdClass();
|
||||
|
||||
$retVal[$i]->name = $query[$i]->COLUMN_NAME;
|
||||
$retVal[$i]->type = $query[$i]->DATA_TYPE;
|
||||
$retVal[$i]->default = $query[$i]->COLUMN_DEFAULT;
|
||||
|
||||
$retVal[$i]->max_length = $query[$i]->CHARACTER_MAXIMUM_LENGTH > 0
|
||||
? $query[$i]->CHARACTER_MAXIMUM_LENGTH
|
||||
: $query[$i]->NUMERIC_PRECISION;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin Transaction
|
||||
*/
|
||||
protected function _transBegin(): bool
|
||||
{
|
||||
return sqlsrv_begin_transaction($this->connID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit Transaction
|
||||
*/
|
||||
protected function _transCommit(): bool
|
||||
{
|
||||
return sqlsrv_commit($this->connID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback Transaction
|
||||
*/
|
||||
protected function _transRollback(): bool
|
||||
{
|
||||
return sqlsrv_rollback($this->connID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last error code and message.
|
||||
* Must return this format: ['code' => string|int, 'message' => string]
|
||||
* intval(code) === 0 means "no error".
|
||||
*
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
public function error(): array
|
||||
{
|
||||
$error = [
|
||||
'code' => '00000',
|
||||
'message' => '',
|
||||
];
|
||||
|
||||
$sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
|
||||
|
||||
if (! is_array($sqlsrvErrors)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$sqlsrvError = array_shift($sqlsrvErrors);
|
||||
if (isset($sqlsrvError['SQLSTATE'])) {
|
||||
$error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
|
||||
} elseif (isset($sqlsrvError['code'])) {
|
||||
$error['code'] = $sqlsrvError['code'];
|
||||
}
|
||||
|
||||
if (isset($sqlsrvError['message'])) {
|
||||
$error['message'] = $sqlsrvError['message'];
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of rows affected by this query.
|
||||
*/
|
||||
public function affectedRows(): int
|
||||
{
|
||||
return sqlsrv_rows_affected($this->resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific database table to use.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function setDatabase(?string $databaseName = null)
|
||||
{
|
||||
if (empty($databaseName)) {
|
||||
$databaseName = $this->database;
|
||||
}
|
||||
|
||||
if (empty($this->connID)) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
if ($this->execute('USE ' . $this->_escapeString($databaseName))) {
|
||||
$this->database = $databaseName;
|
||||
$this->dataCache = [];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the query against the database.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function execute(string $sql)
|
||||
{
|
||||
$stmt = ($this->scrollable === false || $this->isWriteType($sql)) ?
|
||||
sqlsrv_query($this->connID, $sql) :
|
||||
sqlsrv_query($this->connID, $sql, [], ['Scrollable' => $this->scrollable]);
|
||||
|
||||
if ($stmt === false) {
|
||||
$error = $this->error();
|
||||
|
||||
log_message('error', $error['message']);
|
||||
if ($this->DBDebug) {
|
||||
throw new Exception($error['message']);
|
||||
}
|
||||
}
|
||||
|
||||
return $stmt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last error encountered by this connection.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getError()
|
||||
{
|
||||
$error = [
|
||||
'code' => '00000',
|
||||
'message' => '',
|
||||
];
|
||||
|
||||
$sqlsrvErrors = sqlsrv_errors(SQLSRV_ERR_ERRORS);
|
||||
|
||||
if (! is_array($sqlsrvErrors)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$sqlsrvError = array_shift($sqlsrvErrors);
|
||||
if (isset($sqlsrvError['SQLSTATE'])) {
|
||||
$error['code'] = isset($sqlsrvError['code']) ? $sqlsrvError['SQLSTATE'] . '/' . $sqlsrvError['code'] : $sqlsrvError['SQLSTATE'];
|
||||
} elseif (isset($sqlsrvError['code'])) {
|
||||
$error['code'] = $sqlsrvError['code'];
|
||||
}
|
||||
|
||||
if (isset($sqlsrvError['message'])) {
|
||||
$error['message'] = $sqlsrvError['message'];
|
||||
}
|
||||
|
||||
return $error;
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the platform in use (MySQLi, mssql, etc)
|
||||
*/
|
||||
public function getPlatform(): string
|
||||
{
|
||||
return $this->DBDriver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the version of the database being used.
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
if (isset($this->dataCache['version'])) {
|
||||
return $this->dataCache['version'];
|
||||
}
|
||||
|
||||
if (! $this->connID || empty($info = sqlsrv_server_info($this->connID))) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
return isset($info['SQLServerVersion']) ? $this->dataCache['version'] = $info['SQLServerVersion'] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a query is a "write" type.
|
||||
*
|
||||
* Overrides BaseConnection::isWriteType, adding additional read query types.
|
||||
*
|
||||
* @param mixed $sql
|
||||
*/
|
||||
public function isWriteType($sql): bool
|
||||
{
|
||||
if (preg_match('/^\s*"?(EXEC\s*sp_rename)\s/i', $sql)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return parent::isWriteType($sql);
|
||||
}
|
||||
}
|
||||
405
system/Database/SQLSRV/Forge.php
Normal file
405
system/Database/SQLSRV/Forge.php
Normal file
@@ -0,0 +1,405 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLSRV;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Forge as BaseForge;
|
||||
|
||||
/**
|
||||
* Forge for SQLSRV
|
||||
*/
|
||||
class Forge extends BaseForge
|
||||
{
|
||||
/**
|
||||
* DROP CONSTRAINT statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dropConstraintStr;
|
||||
|
||||
/**
|
||||
* DROP INDEX statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dropIndexStr;
|
||||
|
||||
/**
|
||||
* CREATE DATABASE IF statement
|
||||
*
|
||||
* @todo missing charset, collat & check for existent
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $createDatabaseIfStr = "DECLARE @DBName VARCHAR(255) = '%s'\nDECLARE @SQL VARCHAR(max) = 'IF DB_ID( ''' + @DBName + ''' ) IS NULL CREATE DATABASE ' + @DBName\nEXEC( @SQL )";
|
||||
|
||||
/**
|
||||
* CREATE DATABASE IF statement
|
||||
*
|
||||
* @todo missing charset & collat
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $createDatabaseStr = 'CREATE DATABASE %s ';
|
||||
|
||||
/**
|
||||
* CHECK DATABASE EXIST statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $checkDatabaseExistStr = 'IF DB_ID( %s ) IS NOT NULL SELECT 1';
|
||||
|
||||
/**
|
||||
* RENAME TABLE statement
|
||||
*
|
||||
* While the below statement would work, it returns an error.
|
||||
* Also MS recommends dropping and dropping and re-creating the table.
|
||||
*
|
||||
* @see https://docs.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-rename-transact-sql?view=sql-server-2017
|
||||
* 'EXEC sp_rename %s , %s ;'
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $renameTableStr;
|
||||
|
||||
/**
|
||||
* UNSIGNED support
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $unsigned = [
|
||||
'TINYINT' => 'SMALLINT',
|
||||
'SMALLINT' => 'INT',
|
||||
'INT' => 'BIGINT',
|
||||
'REAL' => 'FLOAT',
|
||||
];
|
||||
|
||||
/**
|
||||
* CREATE TABLE IF statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $createTableIfStr;
|
||||
|
||||
/**
|
||||
* CREATE TABLE statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $createTableStr;
|
||||
|
||||
public function __construct(BaseConnection $db)
|
||||
{
|
||||
parent::__construct($db);
|
||||
|
||||
$this->createTableIfStr = 'IF NOT EXISTS'
|
||||
. '(SELECT t.name, s.name as schema_name, t.type_desc '
|
||||
. 'FROM sys.tables t '
|
||||
. 'INNER JOIN sys.schemas s on s.schema_id = t.schema_id '
|
||||
. "WHERE s.name=N'" . $this->db->schema . "' "
|
||||
. "AND t.name=REPLACE(N'%s', '\"', '') "
|
||||
. "AND t.type_desc='USER_TABLE')\nCREATE TABLE ";
|
||||
|
||||
$this->createTableStr = '%s ' . $this->db->escapeIdentifiers($this->db->schema) . ".%s (%s\n) ";
|
||||
$this->renameTableStr = 'EXEC sp_rename [' . $this->db->escapeIdentifiers($this->db->schema) . '.%s] , %s ;';
|
||||
|
||||
$this->dropConstraintStr = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.%s DROP CONSTRAINT %s';
|
||||
$this->dropIndexStr = 'DROP INDEX %s ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.%s';
|
||||
}
|
||||
|
||||
/**
|
||||
* CREATE TABLE attributes
|
||||
*/
|
||||
protected function _createTableAttributes(array $attributes): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $field
|
||||
*
|
||||
* @return false|string|string[]
|
||||
*/
|
||||
protected function _alterTable(string $alterType, string $table, $field)
|
||||
{
|
||||
|
||||
// Handle DROP here
|
||||
if ($alterType === 'DROP') {
|
||||
// check if fields are part of any indexes
|
||||
$indexData = $this->db->getIndexData($table);
|
||||
|
||||
foreach ($indexData as $index) {
|
||||
if (is_string($field)) {
|
||||
$field = explode(',', $field);
|
||||
}
|
||||
|
||||
$fld = array_intersect($field, $index->fields);
|
||||
|
||||
// Drop index if field is part of an index
|
||||
if (! empty($fld)) {
|
||||
$this->_dropIndex($table, $index);
|
||||
}
|
||||
}
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table) . ' DROP ';
|
||||
|
||||
$fields = array_map(static function ($item) {
|
||||
return 'COLUMN [' . trim($item) . ']';
|
||||
}, (array) $field);
|
||||
|
||||
return $sql . implode(',', $fields);
|
||||
}
|
||||
|
||||
$sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table);
|
||||
$sql .= ($alterType === 'ADD') ? 'ADD ' : ' ';
|
||||
|
||||
$sqls = [];
|
||||
|
||||
if ($alterType === 'ADD') {
|
||||
foreach ($field as $data) {
|
||||
$sqls[] = $sql . ($data['_literal'] !== false ? $data['_literal'] : $this->_processColumn($data));
|
||||
}
|
||||
|
||||
return $sqls;
|
||||
}
|
||||
|
||||
foreach ($field as $data) {
|
||||
if ($data['_literal'] !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($data['type'])) {
|
||||
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
|
||||
. " {$data['type']}{$data['length']}";
|
||||
}
|
||||
|
||||
if (! empty($data['default'])) {
|
||||
$sqls[] = $sql . ' ALTER COLUMN ADD CONSTRAINT ' . $this->db->escapeIdentifiers($data['name']) . '_def'
|
||||
. " DEFAULT {$data['default']} FOR " . $this->db->escapeIdentifiers($data['name']);
|
||||
}
|
||||
|
||||
if (isset($data['null'])) {
|
||||
$sqls[] = $sql . ' ALTER COLUMN ' . $this->db->escapeIdentifiers($data['name'])
|
||||
. ($data['null'] === true ? ' DROP' : '') . " {$data['type']}{$data['length']} NOT NULL";
|
||||
}
|
||||
|
||||
if (! empty($data['comment'])) {
|
||||
$sqls[] = 'EXEC sys.sp_addextendedproperty '
|
||||
. "@name=N'Caption', @value=N'" . $data['comment'] . "' , "
|
||||
. "@level0type=N'SCHEMA',@level0name=N'" . $this->db->schema . "', "
|
||||
. "@level1type=N'TABLE',@level1name=N'" . $this->db->escapeIdentifiers($table) . "', "
|
||||
. "@level2type=N'COLUMN',@level2name=N'" . $this->db->escapeIdentifiers($data['name']) . "'";
|
||||
}
|
||||
|
||||
if (! empty($data['new_name'])) {
|
||||
$sqls[] = "EXEC sp_rename '[" . $this->db->schema . '].[' . $table . '].[' . $data['name'] . "]' , '" . $data['new_name'] . "', 'COLUMN';";
|
||||
}
|
||||
}
|
||||
|
||||
return $sqls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop index for table
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function _dropIndex(string $table, object $indexData)
|
||||
{
|
||||
if ($indexData->type === 'PRIMARY') {
|
||||
$sql = 'ALTER TABLE [' . $this->db->schema . '].[' . $table . '] DROP [' . $indexData->name . ']';
|
||||
} else {
|
||||
$sql = 'DROP INDEX [' . $indexData->name . '] ON [' . $this->db->schema . '].[' . $table . ']';
|
||||
}
|
||||
|
||||
return $this->db->simpleQuery($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process indexes
|
||||
*
|
||||
* @return array|string
|
||||
*/
|
||||
protected function _processIndexes(string $table)
|
||||
{
|
||||
$sqls = [];
|
||||
|
||||
for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
|
||||
$this->keys[$i] = (array) $this->keys[$i];
|
||||
|
||||
for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) {
|
||||
if (! isset($this->fields[$this->keys[$i][$i2]])) {
|
||||
unset($this->keys[$i][$i2]);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($this->keys[$i]) <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($i, $this->uniqueKeys, true)) {
|
||||
$sqls[] = 'ALTER TABLE '
|
||||
. $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table)
|
||||
. ' ADD CONSTRAINT ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
|
||||
. ' UNIQUE (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$sqls[] = 'CREATE INDEX '
|
||||
. $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
|
||||
. ' ON ' . $this->db->escapeIdentifiers($this->db->schema) . '.' . $this->db->escapeIdentifiers($table)
|
||||
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
|
||||
}
|
||||
|
||||
return $sqls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process column
|
||||
*/
|
||||
protected function _processColumn(array $field): string
|
||||
{
|
||||
return $this->db->escapeIdentifiers($field['name'])
|
||||
. (empty($field['new_name']) ? '' : ' ' . $this->db->escapeIdentifiers($field['new_name']))
|
||||
. ' ' . $field['type'] . $field['length']
|
||||
. $field['default']
|
||||
. $field['null']
|
||||
. $field['auto_increment']
|
||||
. ''
|
||||
. $field['unique'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process foreign keys
|
||||
*
|
||||
* @param string $table Table name
|
||||
*/
|
||||
protected function _processForeignKeys(string $table): string
|
||||
{
|
||||
$sql = '';
|
||||
|
||||
$allowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT'];
|
||||
|
||||
foreach ($this->foreignKeys as $fkey) {
|
||||
$nameIndex = $table . '_' . implode('_', $fkey['field']) . '_foreign';
|
||||
$nameIndexFilled = $this->db->escapeIdentifiers($nameIndex);
|
||||
$foreignKeyFilled = implode(', ', $this->db->escapeIdentifiers($fkey['field']));
|
||||
$referenceTableFilled = $this->db->escapeIdentifiers($this->db->DBPrefix . $fkey['referenceTable']);
|
||||
$referenceFieldFilled = implode(', ', $this->db->escapeIdentifiers($fkey['referenceField']));
|
||||
|
||||
$formatSql = ",\n\tCONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s(%s)";
|
||||
$sql .= sprintf($formatSql, $nameIndexFilled, $foreignKeyFilled, $referenceTableFilled, $referenceFieldFilled);
|
||||
|
||||
if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions, true)) {
|
||||
$sql .= ' ON DELETE ' . $fkey['onDelete'];
|
||||
}
|
||||
|
||||
if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions, true)) {
|
||||
$sql .= ' ON UPDATE ' . $fkey['onUpdate'];
|
||||
}
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
|
||||
/**
|
||||
* Process primary keys
|
||||
*/
|
||||
protected function _processPrimaryKeys(string $table): string
|
||||
{
|
||||
for ($i = 0, $c = count($this->primaryKeys); $i < $c; $i++) {
|
||||
if (! isset($this->fields[$this->primaryKeys[$i]])) {
|
||||
unset($this->primaryKeys[$i]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->primaryKeys !== []) {
|
||||
$sql = ",\n\tCONSTRAINT " . $this->db->escapeIdentifiers('pk_' . $table)
|
||||
. ' PRIMARY KEY(' . implode(', ', $this->db->escapeIdentifiers($this->primaryKeys)) . ')';
|
||||
}
|
||||
|
||||
return $sql ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a data type mapping between different databases.
|
||||
*/
|
||||
protected function _attributeType(array &$attributes)
|
||||
{
|
||||
// Reset field lengths for data types that don't support it
|
||||
if (isset($attributes['CONSTRAINT']) && stripos($attributes['TYPE'], 'int') !== false) {
|
||||
$attributes['CONSTRAINT'] = null;
|
||||
}
|
||||
|
||||
switch (strtoupper($attributes['TYPE'])) {
|
||||
case 'MEDIUMINT':
|
||||
$attributes['TYPE'] = 'INTEGER';
|
||||
$attributes['UNSIGNED'] = false;
|
||||
break;
|
||||
|
||||
case 'INTEGER':
|
||||
$attributes['TYPE'] = 'INT';
|
||||
break;
|
||||
|
||||
case 'ENUM':
|
||||
$attributes['TYPE'] = 'TEXT';
|
||||
$attributes['CONSTRAINT'] = null;
|
||||
break;
|
||||
|
||||
case 'TIMESTAMP':
|
||||
$attributes['TYPE'] = 'DATETIME';
|
||||
break;
|
||||
|
||||
case 'BOOLEAN':
|
||||
$attributes['TYPE'] = 'BIT';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Field attribute AUTO_INCREMENT
|
||||
*/
|
||||
protected function _attributeAutoIncrement(array &$attributes, array &$field)
|
||||
{
|
||||
if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true && stripos($field['type'], 'INT') !== false) {
|
||||
$field['auto_increment'] = ' IDENTITY(1,1)';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific DROP TABLE string
|
||||
*
|
||||
* @todo Support for cascade
|
||||
*/
|
||||
protected function _dropTable(string $table, bool $ifExists, bool $cascade): string
|
||||
{
|
||||
$sql = 'DROP TABLE';
|
||||
|
||||
if ($ifExists) {
|
||||
$sql .= ' IF EXISTS ';
|
||||
}
|
||||
|
||||
$table = ' [' . $this->db->database . '].[' . $this->db->schema . '].[' . $table . '] ';
|
||||
|
||||
$sql .= $table;
|
||||
|
||||
if ($cascade) {
|
||||
$sql .= '';
|
||||
}
|
||||
|
||||
return $sql;
|
||||
}
|
||||
}
|
||||
114
system/Database/SQLSRV/PreparedQuery.php
Normal file
114
system/Database/SQLSRV/PreparedQuery.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLSRV;
|
||||
|
||||
use BadMethodCallException;
|
||||
use CodeIgniter\Database\BasePreparedQuery;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Prepared query for Postgre
|
||||
*/
|
||||
class PreparedQuery extends BasePreparedQuery
|
||||
{
|
||||
/**
|
||||
* Parameters array used to store the dynamic variables.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $parameters = [];
|
||||
|
||||
/**
|
||||
* The result boolean from a sqlsrv_execute.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* NOTE: This version is based on SQL code. Child classes should
|
||||
* override this method.
|
||||
*
|
||||
* @param array $options Options takes an associative array;
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _prepare(string $sql, array $options = [])
|
||||
{
|
||||
// Prepare parameters for the query
|
||||
$queryString = $this->getQueryString();
|
||||
|
||||
$parameters = $this->parameterize($queryString);
|
||||
|
||||
// Prepare the query
|
||||
$this->statement = sqlsrv_prepare($this->db->connID, $sql, $parameters);
|
||||
|
||||
if (! $this->statement) {
|
||||
$info = $this->db->error();
|
||||
$this->errorCode = $info['code'];
|
||||
$this->errorString = $info['message'];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*/
|
||||
public function _execute(array $data): bool
|
||||
{
|
||||
if (! isset($this->statement)) {
|
||||
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
|
||||
}
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$this->parameters[$key] = $value;
|
||||
}
|
||||
|
||||
$this->result = sqlsrv_execute($this->statement);
|
||||
|
||||
return (bool) $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result object for the prepared query.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _getResult()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle parameters
|
||||
*/
|
||||
protected function parameterize(string $queryString): array
|
||||
{
|
||||
$numberOfVariables = substr_count($queryString, '?');
|
||||
|
||||
$params = [];
|
||||
|
||||
for ($c = 0; $c < $numberOfVariables; $c++) {
|
||||
$this->parameters[$c] = null;
|
||||
$params[] = &$this->parameters[$c];
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
170
system/Database/SQLSRV/Result.php
Normal file
170
system/Database/SQLSRV/Result.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLSRV;
|
||||
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Result for SQLSRV
|
||||
*/
|
||||
class Result extends BaseResult
|
||||
{
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getFieldCount(): int
|
||||
{
|
||||
return @sqlsrv_num_fields($this->resultID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
public function getFieldNames(): array
|
||||
{
|
||||
$fieldNames = [];
|
||||
|
||||
foreach (sqlsrv_field_metadata($this->resultID) as $field) {
|
||||
$fieldNames[] = $field['Name'];
|
||||
}
|
||||
|
||||
return $fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
public function getFieldData(): array
|
||||
{
|
||||
static $dataTypes = [
|
||||
SQLSRV_SQLTYPE_BIGINT => 'bigint',
|
||||
SQLSRV_SQLTYPE_BIT => 'bit',
|
||||
SQLSRV_SQLTYPE_CHAR => 'char',
|
||||
|
||||
SQLSRV_SQLTYPE_DATE => 'date',
|
||||
SQLSRV_SQLTYPE_DATETIME => 'datetime',
|
||||
SQLSRV_SQLTYPE_DATETIME2 => 'datetime2',
|
||||
SQLSRV_SQLTYPE_DATETIMEOFFSET => 'datetimeoffset',
|
||||
|
||||
SQLSRV_SQLTYPE_DECIMAL => 'decimal',
|
||||
SQLSRV_SQLTYPE_FLOAT => 'float',
|
||||
|
||||
SQLSRV_SQLTYPE_IMAGE => 'image',
|
||||
SQLSRV_SQLTYPE_INT => 'int',
|
||||
SQLSRV_SQLTYPE_MONEY => 'money',
|
||||
SQLSRV_SQLTYPE_NCHAR => 'nchar',
|
||||
SQLSRV_SQLTYPE_NUMERIC => 'numeric',
|
||||
|
||||
SQLSRV_SQLTYPE_NVARCHAR => 'nvarchar',
|
||||
SQLSRV_SQLTYPE_NTEXT => 'ntext',
|
||||
|
||||
SQLSRV_SQLTYPE_REAL => 'real',
|
||||
SQLSRV_SQLTYPE_SMALLDATETIME => 'smalldatetime',
|
||||
SQLSRV_SQLTYPE_SMALLINT => 'smallint',
|
||||
SQLSRV_SQLTYPE_SMALLMONEY => 'smallmoney',
|
||||
SQLSRV_SQLTYPE_TEXT => 'text',
|
||||
|
||||
SQLSRV_SQLTYPE_TIME => 'time',
|
||||
SQLSRV_SQLTYPE_TIMESTAMP => 'timestamp',
|
||||
SQLSRV_SQLTYPE_TINYINT => 'tinyint',
|
||||
SQLSRV_SQLTYPE_UNIQUEIDENTIFIER => 'uniqueidentifier',
|
||||
SQLSRV_SQLTYPE_UDT => 'udt',
|
||||
SQLSRV_SQLTYPE_VARBINARY => 'varbinary',
|
||||
SQLSRV_SQLTYPE_VARCHAR => 'varchar',
|
||||
SQLSRV_SQLTYPE_XML => 'xml',
|
||||
];
|
||||
|
||||
$retVal = [];
|
||||
|
||||
foreach (sqlsrv_field_metadata($this->resultID) as $i => $field) {
|
||||
$retVal[$i] = new stdClass();
|
||||
|
||||
$retVal[$i]->name = $field['Name'];
|
||||
$retVal[$i]->type = $field['Type'];
|
||||
$retVal[$i]->type_name = $dataTypes[$field['Type']] ?? null;
|
||||
$retVal[$i]->max_length = $field['Size'];
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*/
|
||||
public function freeResult()
|
||||
{
|
||||
if (is_resource($this->resultID)) {
|
||||
sqlsrv_free_stmt($this->resultID);
|
||||
$this->resultID = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function dataSeek(int $n = 0)
|
||||
{
|
||||
if ($n > 0) {
|
||||
for ($i = 0; $i < $n; $i++) {
|
||||
if (sqlsrv_fetch($this->resultID) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an array.
|
||||
*
|
||||
* Overridden by driver classes.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fetchAssoc()
|
||||
{
|
||||
return sqlsrv_fetch_array($this->resultID, SQLSRV_FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an object.
|
||||
*
|
||||
* @return bool|Entity|object
|
||||
*/
|
||||
protected function fetchObject(string $className = 'stdClass')
|
||||
{
|
||||
if (is_subclass_of($className, Entity::class)) {
|
||||
return empty($data = $this->fetchAssoc()) ? false : (new $className())->setAttributes($data);
|
||||
}
|
||||
|
||||
return sqlsrv_fetch_object($this->resultID, $className);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of rows in the resultID (i.e., SQLSRV query result resource)
|
||||
*/
|
||||
public function getNumRows(): int
|
||||
{
|
||||
if (! is_int($this->numRows)) {
|
||||
$this->numRows = sqlsrv_num_rows($this->resultID);
|
||||
}
|
||||
|
||||
return $this->numRows;
|
||||
}
|
||||
}
|
||||
53
system/Database/SQLSRV/Utils.php
Normal file
53
system/Database/SQLSRV/Utils.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLSRV;
|
||||
|
||||
use CodeIgniter\Database\BaseUtils;
|
||||
use CodeIgniter\Database\ConnectionInterface;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* Utils for SQLSRV
|
||||
*/
|
||||
class Utils extends BaseUtils
|
||||
{
|
||||
/**
|
||||
* List databases statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $listDatabases = 'EXEC sp_helpdb'; // Can also be: EXEC sp_databases
|
||||
|
||||
/**
|
||||
* OPTIMIZE TABLE statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $optimizeTable = 'ALTER INDEX all ON %s REORGANIZE';
|
||||
|
||||
public function __construct(ConnectionInterface &$db)
|
||||
{
|
||||
parent::__construct($db);
|
||||
|
||||
$this->optimizeTable = 'ALTER INDEX all ON ' . $this->db->schema . '.%s REORGANIZE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform dependent version of the backup function.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _backup(?array $prefs = null)
|
||||
{
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
}
|
||||
73
system/Database/SQLite3/Builder.php
Normal file
73
system/Database/SQLite3/Builder.php
Normal file
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
|
||||
/**
|
||||
* Builder for SQLite3
|
||||
*/
|
||||
class Builder extends BaseBuilder
|
||||
{
|
||||
/**
|
||||
* Default installs of SQLite typically do not
|
||||
* support limiting delete clauses.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $canLimitDeletes = false;
|
||||
|
||||
/**
|
||||
* Default installs of SQLite do no support
|
||||
* limiting update queries in combo with WHERE.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $canLimitWhereUpdates = false;
|
||||
|
||||
/**
|
||||
* ORDER BY random keyword
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $randomKeyword = [
|
||||
'RANDOM()',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $supportedIgnoreStatements = [
|
||||
'insert' => 'OR IGNORE',
|
||||
];
|
||||
|
||||
/**
|
||||
* Replace statement
|
||||
*
|
||||
* Generates a platform-specific replace string from the supplied data
|
||||
*/
|
||||
protected function _replace(string $table, array $keys, array $values): string
|
||||
{
|
||||
return 'INSERT OR ' . parent::_replace($table, $keys, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific truncate string from the supplied data
|
||||
*
|
||||
* If the database does not support the TRUNCATE statement,
|
||||
* then this method maps to 'DELETE FROM table'
|
||||
*/
|
||||
protected function _truncate(string $table): string
|
||||
{
|
||||
return 'DELETE FROM ' . $table;
|
||||
}
|
||||
}
|
||||
396
system/Database/SQLite3/Connection.php
Normal file
396
system/Database/SQLite3/Connection.php
Normal file
@@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use ErrorException;
|
||||
use Exception;
|
||||
use SQLite3;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Connection for SQLite3
|
||||
*/
|
||||
class Connection extends BaseConnection
|
||||
{
|
||||
/**
|
||||
* Database driver
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $DBDriver = 'SQLite3';
|
||||
|
||||
/**
|
||||
* Identifier escape character
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $escapeChar = '`';
|
||||
|
||||
/**
|
||||
* Connect to the database.
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function connect(bool $persistent = false)
|
||||
{
|
||||
if ($persistent && $this->DBDebug) {
|
||||
throw new DatabaseException('SQLite3 doesn\'t support persistent connections.');
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->database !== ':memory:' && strpos($this->database, DIRECTORY_SEPARATOR) === false) {
|
||||
$this->database = WRITEPATH . $this->database;
|
||||
}
|
||||
|
||||
return (! $this->password)
|
||||
? new SQLite3($this->database)
|
||||
: new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
|
||||
} catch (Exception $e) {
|
||||
throw new DatabaseException('SQLite3 error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep or establish the connection if no queries have been sent for
|
||||
* a length of time exceeding the server's idle timeout.
|
||||
*/
|
||||
public function reconnect()
|
||||
{
|
||||
$this->close();
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection.
|
||||
*/
|
||||
protected function _close()
|
||||
{
|
||||
$this->connID->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific database table to use.
|
||||
*/
|
||||
public function setDatabase(string $databaseName): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the version of the database being used.
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
if (isset($this->dataCache['version'])) {
|
||||
return $this->dataCache['version'];
|
||||
}
|
||||
|
||||
$version = SQLite3::version();
|
||||
|
||||
return $this->dataCache['version'] = $version['versionString'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query
|
||||
*
|
||||
* @return mixed \SQLite3Result object or bool
|
||||
*/
|
||||
protected function execute(string $sql)
|
||||
{
|
||||
try {
|
||||
return $this->isWriteType($sql)
|
||||
? $this->connID->exec($sql)
|
||||
: $this->connID->query($sql);
|
||||
} catch (ErrorException $e) {
|
||||
log_message('error', $e);
|
||||
if ($this->DBDebug) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of rows affected by this query.
|
||||
*/
|
||||
public function affectedRows(): int
|
||||
{
|
||||
return $this->connID->changes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-dependant string escape
|
||||
*/
|
||||
protected function _escapeString(string $str): string
|
||||
{
|
||||
return $this->connID->escapeString($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the SQL for listing tables in a platform-dependent manner.
|
||||
*/
|
||||
protected function _listTables(bool $prefixLimit = false): string
|
||||
{
|
||||
return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
|
||||
. ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
|
||||
. (($prefixLimit !== false && $this->DBPrefix !== '')
|
||||
? ' AND "NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . '%\' ' . sprintf($this->likeEscapeStr, $this->likeEscapeChar)
|
||||
: '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific query string so that the column names can be fetched.
|
||||
*/
|
||||
protected function _listColumns(string $table = ''): string
|
||||
{
|
||||
return 'PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
public function getFieldNames(string $table)
|
||||
{
|
||||
// Is there a cached result?
|
||||
if (isset($this->dataCache['field_names'][$table])) {
|
||||
return $this->dataCache['field_names'][$table];
|
||||
}
|
||||
|
||||
if (empty($this->connID)) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
$sql = $this->_listColumns($table);
|
||||
|
||||
$query = $this->query($sql);
|
||||
$this->dataCache['field_names'][$table] = [];
|
||||
|
||||
foreach ($query->getResultArray() as $row) {
|
||||
// Do we know from where to get the column's name?
|
||||
if (! isset($key)) {
|
||||
if (isset($row['column_name'])) {
|
||||
$key = 'column_name';
|
||||
} elseif (isset($row['COLUMN_NAME'])) {
|
||||
$key = 'COLUMN_NAME';
|
||||
} elseif (isset($row['name'])) {
|
||||
$key = 'name';
|
||||
} else {
|
||||
// We have no other choice but to just get the first element's key.
|
||||
$key = key($row);
|
||||
}
|
||||
}
|
||||
|
||||
$this->dataCache['field_names'][$table][] = $row[$key];
|
||||
}
|
||||
|
||||
return $this->dataCache['field_names'][$table];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with field data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _fieldData(string $table): array
|
||||
{
|
||||
if (false === $query = $this->query('PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')')) {
|
||||
throw new DatabaseException(lang('Database.failGetFieldData'));
|
||||
}
|
||||
|
||||
$query = $query->getResultObject();
|
||||
|
||||
if (empty($query)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$retVal = [];
|
||||
|
||||
for ($i = 0, $c = count($query); $i < $c; $i++) {
|
||||
$retVal[$i] = new stdClass();
|
||||
|
||||
$retVal[$i]->name = $query[$i]->name;
|
||||
$retVal[$i]->type = $query[$i]->type;
|
||||
$retVal[$i]->max_length = null;
|
||||
$retVal[$i]->default = $query[$i]->dflt_value;
|
||||
$retVal[$i]->primary_key = isset($query[$i]->pk) && (bool) $query[$i]->pk;
|
||||
$retVal[$i]->nullable = isset($query[$i]->notnull) && ! (bool) $query[$i]->notnull;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with index data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _indexData(string $table): array
|
||||
{
|
||||
// Get indexes
|
||||
// Don't use PRAGMA index_list, so we can preserve index order
|
||||
$sql = "SELECT name FROM sqlite_master WHERE type='index' AND tbl_name=" . $this->escape(strtolower($table));
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetIndexData'));
|
||||
}
|
||||
$query = $query->getResultObject();
|
||||
|
||||
$retVal = [];
|
||||
|
||||
foreach ($query as $row) {
|
||||
$obj = new stdClass();
|
||||
|
||||
$obj->name = $row->name;
|
||||
|
||||
// Get fields for index
|
||||
$obj->fields = [];
|
||||
|
||||
if (false === $fields = $this->query('PRAGMA index_info(' . $this->escape(strtolower($row->name)) . ')')) {
|
||||
throw new DatabaseException(lang('Database.failGetIndexData'));
|
||||
}
|
||||
|
||||
$fields = $fields->getResultObject();
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$obj->fields[] = $field->name;
|
||||
}
|
||||
|
||||
$retVal[$obj->name] = $obj;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with Foreign key data
|
||||
*
|
||||
* @return stdClass[]
|
||||
*/
|
||||
protected function _foreignKeyData(string $table): array
|
||||
{
|
||||
if ($this->supportsForeignKeys() !== true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tables = $this->listTables();
|
||||
|
||||
if (empty($tables)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$retVal = [];
|
||||
|
||||
foreach ($tables as $table) {
|
||||
$query = $this->query("PRAGMA foreign_key_list({$table})")->getResult();
|
||||
|
||||
foreach ($query as $row) {
|
||||
$obj = new stdClass();
|
||||
$obj->constraint_name = $row->from . ' to ' . $row->table . '.' . $row->to;
|
||||
$obj->table_name = $table;
|
||||
$obj->foreign_table_name = $row->table;
|
||||
$obj->sequence = $row->seq;
|
||||
|
||||
$retVal[] = $obj;
|
||||
}
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform-specific SQL to disable foreign key checks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _disableForeignKeyChecks()
|
||||
{
|
||||
return 'PRAGMA foreign_keys = OFF';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform-specific SQL to enable foreign key checks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _enableForeignKeyChecks()
|
||||
{
|
||||
return 'PRAGMA foreign_keys = ON';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last error code and message.
|
||||
* Must return this format: ['code' => string|int, 'message' => string]
|
||||
* intval(code) === 0 means "no error".
|
||||
*
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
public function error(): array
|
||||
{
|
||||
return [
|
||||
'code' => $this->connID->lastErrorCode(),
|
||||
'message' => $this->connID->lastErrorMsg(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert ID
|
||||
*/
|
||||
public function insertID(): int
|
||||
{
|
||||
return $this->connID->lastInsertRowID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin Transaction
|
||||
*/
|
||||
protected function _transBegin(): bool
|
||||
{
|
||||
return $this->connID->exec('BEGIN TRANSACTION');
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit Transaction
|
||||
*/
|
||||
protected function _transCommit(): bool
|
||||
{
|
||||
return $this->connID->exec('END TRANSACTION');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback Transaction
|
||||
*/
|
||||
protected function _transRollback(): bool
|
||||
{
|
||||
return $this->connID->exec('ROLLBACK');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the current install supports Foreign Keys
|
||||
* and has them enabled.
|
||||
*/
|
||||
public function supportsForeignKeys(): bool
|
||||
{
|
||||
$result = $this->simpleQuery('PRAGMA foreign_keys');
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
}
|
||||
254
system/Database/SQLite3/Forge.php
Normal file
254
system/Database/SQLite3/Forge.php
Normal file
@@ -0,0 +1,254 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\Forge as BaseForge;
|
||||
|
||||
/**
|
||||
* Forge for SQLite3
|
||||
*/
|
||||
class Forge extends BaseForge
|
||||
{
|
||||
/**
|
||||
* DROP INDEX statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dropIndexStr = 'DROP INDEX %s';
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* UNSIGNED support
|
||||
*
|
||||
* @var array|bool
|
||||
*/
|
||||
protected $_unsigned = false;
|
||||
|
||||
/**
|
||||
* NULL value representation in CREATE/ALTER TABLE statements
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected $null = 'NULL';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(BaseConnection $db)
|
||||
{
|
||||
parent::__construct($db);
|
||||
|
||||
if (version_compare($this->db->getVersion(), '3.3', '<')) {
|
||||
$this->createTableIfStr = false;
|
||||
$this->dropTableIfStr = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create database
|
||||
*
|
||||
* @param bool $ifNotExists Whether to add IF NOT EXISTS condition
|
||||
*/
|
||||
public function createDatabase(string $dbName, bool $ifNotExists = false): bool
|
||||
{
|
||||
// In SQLite, a database is created when you connect to the database.
|
||||
// We'll return TRUE so that an error isn't generated.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop database
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function dropDatabase(string $dbName): bool
|
||||
{
|
||||
// In SQLite, a database is dropped when we delete a file
|
||||
if (! is_file($dbName)) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unable to drop the specified database.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// We need to close the pseudo-connection first
|
||||
$this->db->close();
|
||||
if (! @unlink($dbName)) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unable to drop the specified database.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! empty($this->db->dataCache['db_names'])) {
|
||||
$key = array_search(strtolower($dbName), array_map('strtolower', $this->db->dataCache['db_names']), true);
|
||||
if ($key !== false) {
|
||||
unset($this->db->dataCache['db_names'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $field
|
||||
*
|
||||
* @return array|string|null
|
||||
*/
|
||||
protected function _alterTable(string $alterType, string $table, $field)
|
||||
{
|
||||
switch ($alterType) {
|
||||
case 'DROP':
|
||||
$sqlTable = new Table($this->db, $this);
|
||||
|
||||
$sqlTable->fromTable($table)
|
||||
->dropColumn($field)
|
||||
->run();
|
||||
|
||||
return '';
|
||||
|
||||
case 'CHANGE':
|
||||
(new Table($this->db, $this))
|
||||
->fromTable($table)
|
||||
->modifyColumn($field)
|
||||
->run();
|
||||
|
||||
return null;
|
||||
|
||||
default:
|
||||
return parent::_alterTable($alterType, $table, $field);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process column
|
||||
*/
|
||||
protected function _processColumn(array $field): string
|
||||
{
|
||||
if ($field['type'] === 'TEXT' && strpos($field['length'], "('") === 0) {
|
||||
$field['type'] .= ' CHECK(' . $this->db->escapeIdentifiers($field['name'])
|
||||
. ' IN ' . $field['length'] . ')';
|
||||
}
|
||||
|
||||
return $this->db->escapeIdentifiers($field['name'])
|
||||
. ' ' . $field['type']
|
||||
. $field['auto_increment']
|
||||
. $field['null']
|
||||
. $field['unique']
|
||||
. $field['default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Process indexes
|
||||
*/
|
||||
protected function _processIndexes(string $table): array
|
||||
{
|
||||
$sqls = [];
|
||||
|
||||
for ($i = 0, $c = count($this->keys); $i < $c; $i++) {
|
||||
$this->keys[$i] = (array) $this->keys[$i];
|
||||
|
||||
for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) {
|
||||
if (! isset($this->fields[$this->keys[$i][$i2]])) {
|
||||
unset($this->keys[$i][$i2]);
|
||||
}
|
||||
}
|
||||
if (count($this->keys[$i]) <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($i, $this->uniqueKeys, true)) {
|
||||
$sqls[] = 'CREATE UNIQUE INDEX ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
|
||||
. ' ON ' . $this->db->escapeIdentifiers($table)
|
||||
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$sqls[] = 'CREATE INDEX ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i]))
|
||||
. ' ON ' . $this->db->escapeIdentifiers($table)
|
||||
. ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');';
|
||||
}
|
||||
|
||||
return $sqls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Field attribute TYPE
|
||||
*
|
||||
* Performs a data type mapping between different databases.
|
||||
*/
|
||||
protected function _attributeType(array &$attributes)
|
||||
{
|
||||
switch (strtoupper($attributes['TYPE'])) {
|
||||
case 'ENUM':
|
||||
case 'SET':
|
||||
$attributes['TYPE'] = 'TEXT';
|
||||
break;
|
||||
|
||||
case 'BOOLEAN':
|
||||
$attributes['TYPE'] = 'INT';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Field attribute AUTO_INCREMENT
|
||||
*/
|
||||
protected function _attributeAutoIncrement(array &$attributes, array &$field)
|
||||
{
|
||||
if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true
|
||||
&& stripos($field['type'], 'int') !== false) {
|
||||
$field['type'] = 'INTEGER PRIMARY KEY';
|
||||
$field['default'] = '';
|
||||
$field['null'] = '';
|
||||
$field['unique'] = '';
|
||||
$field['auto_increment'] = ' AUTOINCREMENT';
|
||||
|
||||
$this->primaryKeys = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Foreign Key Drop
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function dropForeignKey(string $table, string $foreignName): bool
|
||||
{
|
||||
// If this version of SQLite doesn't support it, we're done here
|
||||
if ($this->db->supportsForeignKeys() !== true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise we have to copy the table and recreate
|
||||
// without the foreign key being involved now
|
||||
$sqlTable = new Table($this->db, $this);
|
||||
|
||||
return $sqlTable->fromTable($this->db->DBPrefix . $table)
|
||||
->dropForeignKey($foreignName)
|
||||
->run();
|
||||
}
|
||||
}
|
||||
91
system/Database/SQLite3/PreparedQuery.php
Normal file
91
system/Database/SQLite3/PreparedQuery.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use BadMethodCallException;
|
||||
use CodeIgniter\Database\BasePreparedQuery;
|
||||
|
||||
/**
|
||||
* Prepared query for SQLite3
|
||||
*/
|
||||
class PreparedQuery extends BasePreparedQuery
|
||||
{
|
||||
/**
|
||||
* The SQLite3Result resource, or false.
|
||||
*
|
||||
* @var bool|Result
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* NOTE: This version is based on SQL code. Child classes should
|
||||
* override this method.
|
||||
*
|
||||
* @param array $options Passed to the connection's prepare statement.
|
||||
* Unused in the MySQLi driver.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function _prepare(string $sql, array $options = [])
|
||||
{
|
||||
if (! ($this->statement = $this->db->connID->prepare($sql))) {
|
||||
$this->errorCode = $this->db->connID->lastErrorCode();
|
||||
$this->errorString = $this->db->connID->lastErrorMsg();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*
|
||||
* @todo finalize()
|
||||
*/
|
||||
public function _execute(array $data): bool
|
||||
{
|
||||
if (! isset($this->statement)) {
|
||||
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
|
||||
}
|
||||
|
||||
foreach ($data as $key => $item) {
|
||||
// Determine the type string
|
||||
if (is_int($item)) {
|
||||
$bindType = SQLITE3_INTEGER;
|
||||
} elseif (is_float($item)) {
|
||||
$bindType = SQLITE3_FLOAT;
|
||||
} else {
|
||||
$bindType = SQLITE3_TEXT;
|
||||
}
|
||||
|
||||
// Bind it
|
||||
$this->statement->bindValue($key + 1, $item, $bindType);
|
||||
}
|
||||
|
||||
$this->result = $this->statement->execute();
|
||||
|
||||
return $this->result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result object for the prepared query.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _getResult()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
}
|
||||
152
system/Database/SQLite3/Result.php
Normal file
152
system/Database/SQLite3/Result.php
Normal file
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use Closure;
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Result for SQLite3
|
||||
*/
|
||||
class Result extends BaseResult
|
||||
{
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getFieldCount(): int
|
||||
{
|
||||
return $this->resultID->numColumns();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
public function getFieldNames(): array
|
||||
{
|
||||
$fieldNames = [];
|
||||
|
||||
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
|
||||
$fieldNames[] = $this->resultID->columnName($i);
|
||||
}
|
||||
|
||||
return $fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
public function getFieldData(): array
|
||||
{
|
||||
static $dataTypes = [
|
||||
SQLITE3_INTEGER => 'integer',
|
||||
SQLITE3_FLOAT => 'float',
|
||||
SQLITE3_TEXT => 'text',
|
||||
SQLITE3_BLOB => 'blob',
|
||||
SQLITE3_NULL => 'null',
|
||||
];
|
||||
|
||||
$retVal = [];
|
||||
$this->resultID->fetchArray(SQLITE3_NUM);
|
||||
|
||||
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
|
||||
$retVal[$i] = new stdClass();
|
||||
$retVal[$i]->name = $this->resultID->columnName($i);
|
||||
$type = $this->resultID->columnType($i);
|
||||
$retVal[$i]->type = $type;
|
||||
$retVal[$i]->type_name = $dataTypes[$type] ?? null;
|
||||
$retVal[$i]->max_length = null;
|
||||
$retVal[$i]->length = null;
|
||||
}
|
||||
$this->resultID->reset();
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*/
|
||||
public function freeResult()
|
||||
{
|
||||
if (is_object($this->resultID)) {
|
||||
$this->resultID->finalize();
|
||||
$this->resultID = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function dataSeek(int $n = 0)
|
||||
{
|
||||
if ($n !== 0) {
|
||||
throw new DatabaseException('SQLite3 doesn\'t support seeking to other offset.');
|
||||
}
|
||||
|
||||
return $this->resultID->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an array.
|
||||
*
|
||||
* Overridden by driver classes.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function fetchAssoc()
|
||||
{
|
||||
return $this->resultID->fetchArray(SQLITE3_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an object.
|
||||
*
|
||||
* Overridden by child classes.
|
||||
*
|
||||
* @return bool|object
|
||||
*/
|
||||
protected function fetchObject(string $className = 'stdClass')
|
||||
{
|
||||
// No native support for fetching rows as objects
|
||||
if (($row = $this->fetchAssoc()) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($className === 'stdClass') {
|
||||
return (object) $row;
|
||||
}
|
||||
|
||||
$classObj = new $className();
|
||||
|
||||
if (is_subclass_of($className, Entity::class)) {
|
||||
return $classObj->setAttributes($row);
|
||||
}
|
||||
|
||||
$classSet = Closure::bind(function ($key, $value) {
|
||||
$this->{$key} = $value;
|
||||
}, $classObj, $className);
|
||||
|
||||
foreach (array_keys($row) as $key) {
|
||||
$classSet($key, $row[$key]);
|
||||
}
|
||||
|
||||
return $classObj;
|
||||
}
|
||||
}
|
||||
363
system/Database/SQLite3/Table.php
Normal file
363
system/Database/SQLite3/Table.php
Normal file
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\Exceptions\DataException;
|
||||
|
||||
/**
|
||||
* Class Table
|
||||
*
|
||||
* Provides missing features for altering tables that are common
|
||||
* in other supported databases, but are missing from SQLite.
|
||||
* These are needed in order to support migrations during testing
|
||||
* when another database is used as the primary engine, but
|
||||
* SQLite in memory databases are used for faster test execution.
|
||||
*/
|
||||
class Table
|
||||
{
|
||||
/**
|
||||
* All of the fields this table represents.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fields = [];
|
||||
|
||||
/**
|
||||
* All of the unique/primary keys in the table.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $keys = [];
|
||||
|
||||
/**
|
||||
* All of the foreign keys in the table.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $foreignKeys = [];
|
||||
|
||||
/**
|
||||
* The name of the table we're working with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tableName;
|
||||
|
||||
/**
|
||||
* The name of the table, with database prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $prefixedTableName;
|
||||
|
||||
/**
|
||||
* Database connection.
|
||||
*
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* Handle to our forge.
|
||||
*
|
||||
* @var Forge
|
||||
*/
|
||||
protected $forge;
|
||||
|
||||
/**
|
||||
* Table constructor.
|
||||
*/
|
||||
public function __construct(Connection $db, Forge $forge)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->forge = $forge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an existing database table and
|
||||
* collects all of the information needed to
|
||||
* recreate this table.
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function fromTable(string $table)
|
||||
{
|
||||
$this->prefixedTableName = $table;
|
||||
|
||||
$prefix = $this->db->DBPrefix;
|
||||
|
||||
if (! empty($prefix) && strpos($table, $prefix) === 0) {
|
||||
$table = substr($table, strlen($prefix));
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists($this->prefixedTableName)) {
|
||||
throw DataException::forTableNotFound($this->prefixedTableName);
|
||||
}
|
||||
|
||||
$this->tableName = $table;
|
||||
|
||||
$this->fields = $this->formatFields($this->db->getFieldData($table));
|
||||
|
||||
$this->keys = array_merge($this->keys, $this->formatKeys($this->db->getIndexData($table)));
|
||||
|
||||
$this->foreignKeys = $this->db->getForeignKeyData($table);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after `fromTable` and any actions, like `dropColumn`, etc,
|
||||
* to finalize the action. It creates a temp table, creates the new
|
||||
* table with modifications, and copies the data over to the new table.
|
||||
* Resets the connection dataCache to be sure changes are collected.
|
||||
*/
|
||||
public function run(): bool
|
||||
{
|
||||
$this->db->query('PRAGMA foreign_keys = OFF');
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
$this->forge->renameTable($this->tableName, "temp_{$this->tableName}");
|
||||
|
||||
$this->forge->reset();
|
||||
|
||||
$this->createTable();
|
||||
|
||||
$this->copyData();
|
||||
|
||||
$this->forge->dropTable("temp_{$this->tableName}");
|
||||
|
||||
$success = $this->db->transComplete();
|
||||
|
||||
$this->db->query('PRAGMA foreign_keys = ON');
|
||||
|
||||
$this->db->resetDataCache();
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops columns from the table.
|
||||
*
|
||||
* @param array|string $columns
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function dropColumn($columns)
|
||||
{
|
||||
if (is_string($columns)) {
|
||||
$columns = explode(',', $columns);
|
||||
}
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$column = trim($column);
|
||||
if (isset($this->fields[$column])) {
|
||||
unset($this->fields[$column]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a field, including changing data type,
|
||||
* renaming, etc.
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function modifyColumn(array $field)
|
||||
{
|
||||
$field = $field[0];
|
||||
|
||||
$oldName = $field['name'];
|
||||
unset($field['name']);
|
||||
|
||||
$this->fields[$oldName] = $field;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a foreign key from this table so that
|
||||
* it won't be recreated in the future.
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function dropForeignKey(string $column)
|
||||
{
|
||||
if (empty($this->foreignKeys)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
for ($i = 0; $i < count($this->foreignKeys); $i++) {
|
||||
if ($this->foreignKeys[$i]->table_name !== $this->tableName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The column name should be the first thing in the constraint name
|
||||
if (strpos($this->foreignKeys[$i]->constraint_name, $column) !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unset($this->foreignKeys[$i]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the new table based on our current fields.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function createTable()
|
||||
{
|
||||
$this->dropIndexes();
|
||||
$this->db->resetDataCache();
|
||||
|
||||
// Handle any modified columns.
|
||||
$fields = [];
|
||||
|
||||
foreach ($this->fields as $name => $field) {
|
||||
if (isset($field['new_name'])) {
|
||||
$fields[$field['new_name']] = $field;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$fields[$name] = $field;
|
||||
}
|
||||
|
||||
$this->forge->addField($fields);
|
||||
|
||||
// Unique/Index keys
|
||||
if (is_array($this->keys)) {
|
||||
foreach ($this->keys as $key) {
|
||||
switch ($key['type']) {
|
||||
case 'primary':
|
||||
$this->forge->addPrimaryKey($key['fields']);
|
||||
break;
|
||||
|
||||
case 'unique':
|
||||
$this->forge->addUniqueKey($key['fields']);
|
||||
break;
|
||||
|
||||
case 'index':
|
||||
$this->forge->addKey($key['fields']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->forge->createTable($this->tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies data from our old table to the new one,
|
||||
* taking care map data correctly based on any columns
|
||||
* that have been renamed.
|
||||
*/
|
||||
protected function copyData()
|
||||
{
|
||||
$exFields = [];
|
||||
$newFields = [];
|
||||
|
||||
foreach ($this->fields as $name => $details) {
|
||||
$newFields[] = $details['new_name'] ?? $name;
|
||||
$exFields[] = $name;
|
||||
}
|
||||
|
||||
$exFields = implode(', ', $exFields);
|
||||
$newFields = implode(', ', $newFields);
|
||||
|
||||
$this->db->query("INSERT INTO {$this->prefixedTableName}({$newFields}) SELECT {$exFields} FROM {$this->db->DBPrefix}temp_{$this->tableName}");
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts fields retrieved from the database to
|
||||
* the format needed for creating fields with Forge.
|
||||
*
|
||||
* @param array|bool $fields
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function formatFields($fields)
|
||||
{
|
||||
if (! is_array($fields)) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$return[$field->name] = [
|
||||
'type' => $field->type,
|
||||
'default' => $field->default,
|
||||
'null' => $field->nullable,
|
||||
];
|
||||
|
||||
if ($field->primary_key) {
|
||||
$this->keys[$field->name] = [
|
||||
'fields' => [$field->name],
|
||||
'type' => 'primary',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts keys retrieved from the database to
|
||||
* the format needed to create later.
|
||||
*
|
||||
* @param mixed $keys
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function formatKeys($keys)
|
||||
{
|
||||
if (! is_array($keys)) {
|
||||
return $keys;
|
||||
}
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach ($keys as $name => $key) {
|
||||
$return[$name] = [
|
||||
'fields' => $key->fields,
|
||||
'type' => 'index',
|
||||
];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to drop all indexes and constraints
|
||||
* from the database for this table.
|
||||
*/
|
||||
protected function dropIndexes()
|
||||
{
|
||||
if (! is_array($this->keys) || $this->keys === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($this->keys as $name => $key) {
|
||||
if ($key['type'] === 'primary' || $key['type'] === 'unique') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->db->query("DROP INDEX IF EXISTS '{$name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
38
system/Database/SQLite3/Utils.php
Normal file
38
system/Database/SQLite3/Utils.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BaseUtils;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* Utils for SQLite3
|
||||
*/
|
||||
class Utils extends BaseUtils
|
||||
{
|
||||
/**
|
||||
* OPTIMIZE TABLE statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $optimizeTable = 'REINDEX %s';
|
||||
|
||||
/**
|
||||
* Platform dependent version of the backup function.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function _backup(?array $prefs = null)
|
||||
{
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
}
|
||||
193
system/Database/Seeder.php
Normal file
193
system/Database/Seeder.php
Normal file
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database;
|
||||
|
||||
use CodeIgniter\CLI\CLI;
|
||||
use Config\Database;
|
||||
use Faker\Factory;
|
||||
use Faker\Generator;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Class Seeder
|
||||
*/
|
||||
class Seeder
|
||||
{
|
||||
/**
|
||||
* The name of the database group to use.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $DBGroup;
|
||||
|
||||
/**
|
||||
* Where we can find the Seed files.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $seedPath;
|
||||
|
||||
/**
|
||||
* An instance of the main Database configuration
|
||||
*
|
||||
* @var Database
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Database Connection instance
|
||||
*
|
||||
* @var BaseConnection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* Database Forge instance.
|
||||
*
|
||||
* @var Forge
|
||||
*/
|
||||
protected $forge;
|
||||
|
||||
/**
|
||||
* If true, will not display CLI messages.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $silent = false;
|
||||
|
||||
/**
|
||||
* Faker Generator instance.
|
||||
*
|
||||
* @var Generator|null
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
private static $faker;
|
||||
|
||||
/**
|
||||
* Seeder constructor.
|
||||
*/
|
||||
public function __construct(Database $config, ?BaseConnection $db = null)
|
||||
{
|
||||
$this->seedPath = $config->filesPath ?? APPPATH . 'Database/';
|
||||
|
||||
if (empty($this->seedPath)) {
|
||||
throw new InvalidArgumentException('Invalid filesPath set in the Config\Database.');
|
||||
}
|
||||
|
||||
$this->seedPath = rtrim($this->seedPath, '\\/') . '/Seeds/';
|
||||
|
||||
if (! is_dir($this->seedPath)) {
|
||||
throw new InvalidArgumentException('Unable to locate the seeds directory. Please check Config\Database::filesPath');
|
||||
}
|
||||
|
||||
$this->config = &$config;
|
||||
|
||||
$db = $db ?? Database::connect($this->DBGroup);
|
||||
|
||||
$this->db = &$db;
|
||||
$this->forge = Database::forge($this->DBGroup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the Faker Generator instance.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
public static function faker(): ?Generator
|
||||
{
|
||||
if (self::$faker === null && class_exists(Factory::class)) {
|
||||
self::$faker = Factory::create();
|
||||
}
|
||||
|
||||
return self::$faker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the specified seeder and runs it.
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function call(string $class)
|
||||
{
|
||||
$class = trim($class);
|
||||
|
||||
if ($class === '') {
|
||||
throw new InvalidArgumentException('No seeder was specified.');
|
||||
}
|
||||
|
||||
if (strpos($class, '\\') === false) {
|
||||
$path = $this->seedPath . str_replace('.php', '', $class) . '.php';
|
||||
|
||||
if (! is_file($path)) {
|
||||
throw new InvalidArgumentException('The specified seeder is not a valid file: ' . $path);
|
||||
}
|
||||
|
||||
// Assume the class has the correct namespace
|
||||
// @codeCoverageIgnoreStart
|
||||
$class = APP_NAMESPACE . '\Database\Seeds\\' . $class;
|
||||
|
||||
if (! class_exists($class, false)) {
|
||||
require_once $path;
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
|
||||
/** @var Seeder $seeder */
|
||||
$seeder = new $class($this->config);
|
||||
$seeder->setSilent($this->silent)->run();
|
||||
|
||||
unset($seeder);
|
||||
|
||||
if (is_cli() && ! $this->silent) {
|
||||
CLI::write("Seeded: {$class}", 'green');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the location of the directory that seed files can be located in.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setPath(string $path)
|
||||
{
|
||||
$this->seedPath = rtrim($path, '\\/') . '/';
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the silent treatment.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSilent(bool $silent)
|
||||
{
|
||||
$this->silent = $silent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the database seeds. This is where the magic happens.
|
||||
*
|
||||
* Child classes must implement this method and take care
|
||||
* of inserting their data here.
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user