first commit
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BaseBuilder;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\RawSql;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Builder for SQLite3
|
||||
*/
|
||||
class Builder extends BaseBuilder
|
||||
{
|
||||
/**
|
||||
* Default installs of SQLite typically do not
|
||||
* support limiting delete clauses.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $canLimitDeletes = false;
|
||||
|
||||
/**
|
||||
* Default installs of SQLite do no support
|
||||
* limiting update queries in combo with WHERE.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $canLimitWhereUpdates = false;
|
||||
|
||||
/**
|
||||
* ORDER BY random keyword
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $randomKeyword = [
|
||||
'RANDOM()',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $supportedIgnoreStatements = [
|
||||
'insert' => 'OR IGNORE',
|
||||
];
|
||||
|
||||
/**
|
||||
* Replace statement
|
||||
*
|
||||
* Generates a platform-specific replace string from the supplied data
|
||||
*/
|
||||
protected function _replace(string $table, array $keys, array $values): string
|
||||
{
|
||||
return 'INSERT OR ' . parent::_replace($table, $keys, $values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific truncate string from the supplied data
|
||||
*
|
||||
* If the database does not support the TRUNCATE statement,
|
||||
* then this method maps to 'DELETE FROM table'
|
||||
*/
|
||||
protected function _truncate(string $table): string
|
||||
{
|
||||
return 'DELETE FROM ' . $table;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific batch update string from the supplied data
|
||||
*/
|
||||
protected function _updateBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
if (version_compare($this->db->getVersion(), '3.33.0') >= 0) {
|
||||
return parent::_updateBatch($table, $keys, $values);
|
||||
}
|
||||
|
||||
$constraints = $this->QBOptions['constraints'] ?? [];
|
||||
|
||||
if ($constraints === []) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('You must specify a constraint to match on for batch updates.');
|
||||
}
|
||||
|
||||
return ''; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
if (count($constraints) > 1 || isset($this->QBOptions['setQueryAsData']) || (current($constraints) instanceof RawSql)) {
|
||||
throw new DatabaseException('You are trying to use a feature which requires SQLite version 3.33 or higher.');
|
||||
}
|
||||
|
||||
$index = current($constraints);
|
||||
|
||||
$ids = [];
|
||||
$final = [];
|
||||
|
||||
foreach ($values as $val) {
|
||||
$val = array_combine($keys, $val);
|
||||
|
||||
$ids[] = $val[$index];
|
||||
|
||||
foreach (array_keys($val) as $field) {
|
||||
if ($field !== $index) {
|
||||
$final[$field][] = 'WHEN ' . $index . ' = ' . $val[$index] . ' THEN ' . $val[$field];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$cases = '';
|
||||
|
||||
foreach ($final as $k => $v) {
|
||||
$cases .= $k . " = CASE \n"
|
||||
. implode("\n", $v) . "\n"
|
||||
. 'ELSE ' . $k . ' END, ';
|
||||
}
|
||||
|
||||
$this->where($index . ' IN(' . implode(',', $ids) . ')', null, false);
|
||||
|
||||
return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific upsertBatch string from the supplied data
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
protected function _upsertBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
$sql = $this->QBOptions['sql'] ?? '';
|
||||
|
||||
// if this is the first iteration of batch then we need to build skeleton sql
|
||||
if ($sql === '') {
|
||||
$constraints = $this->QBOptions['constraints'] ?? [];
|
||||
|
||||
if (empty($constraints)) {
|
||||
$fieldNames = array_map(static fn ($columnName) => trim($columnName, '`'), $keys);
|
||||
|
||||
$allIndexes = array_filter($this->db->getIndexData($table), static function ($index) use ($fieldNames) {
|
||||
$hasAllFields = count(array_intersect($index->fields, $fieldNames)) === count($index->fields);
|
||||
|
||||
return ($index->type === 'PRIMARY' || $index->type === 'UNIQUE') && $hasAllFields;
|
||||
});
|
||||
|
||||
foreach (array_map(static fn ($index) => $index->fields, $allIndexes) as $index) {
|
||||
$constraints[] = current($index);
|
||||
break;
|
||||
}
|
||||
|
||||
$constraints = $this->onConstraint($constraints)->QBOptions['constraints'] ?? [];
|
||||
}
|
||||
|
||||
if (empty($constraints)) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('No constraint found for upsert.');
|
||||
}
|
||||
|
||||
return ''; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$alias = $this->QBOptions['alias'] ?? '`excluded`';
|
||||
|
||||
if (strtolower($alias) !== '`excluded`') {
|
||||
throw new InvalidArgumentException('SQLite alias is always named "excluded". A custom alias cannot be used.');
|
||||
}
|
||||
|
||||
$updateFields = $this->QBOptions['updateFields'] ??
|
||||
$this->updateFields($keys, false, $constraints)->QBOptions['updateFields'] ??
|
||||
[];
|
||||
|
||||
$sql = 'INSERT INTO ' . $table . ' (';
|
||||
|
||||
$sql .= implode(', ', array_map(static fn ($columnName) => $columnName, $keys));
|
||||
|
||||
$sql .= ")\n";
|
||||
|
||||
$sql .= '{:_table_:}';
|
||||
|
||||
$sql .= 'ON CONFLICT(' . implode(',', $constraints) . ")\n";
|
||||
|
||||
$sql .= "DO UPDATE SET\n";
|
||||
|
||||
$sql .= implode(
|
||||
",\n",
|
||||
array_map(
|
||||
static fn ($key, $value) => $key . ($value instanceof RawSql ?
|
||||
" = {$value}" :
|
||||
" = {$alias}.{$value}"),
|
||||
array_keys($updateFields),
|
||||
$updateFields
|
||||
)
|
||||
);
|
||||
|
||||
$this->QBOptions['sql'] = $sql;
|
||||
}
|
||||
|
||||
if (isset($this->QBOptions['setQueryAsData'])) {
|
||||
$hasWhere = stripos($this->QBOptions['setQueryAsData'], 'WHERE') > 0;
|
||||
|
||||
$data = $this->QBOptions['setQueryAsData'] . ($hasWhere ? '' : "\nWHERE 1 = 1\n");
|
||||
} else {
|
||||
$data = 'VALUES ' . implode(', ', $this->formatValues($values)) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific batch update string from the supplied data
|
||||
*/
|
||||
protected function _deleteBatch(string $table, array $keys, array $values): string
|
||||
{
|
||||
$sql = $this->QBOptions['sql'] ?? '';
|
||||
|
||||
// if this is the first iteration of batch then we need to build skeleton sql
|
||||
if ($sql === '') {
|
||||
$constraints = $this->QBOptions['constraints'] ?? [];
|
||||
|
||||
if ($constraints === []) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('You must specify a constraint to match on for batch deletes.'); // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return ''; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$sql = 'DELETE FROM ' . $table . "\n";
|
||||
|
||||
if (current($constraints) instanceof RawSql && $this->db->DBDebug) {
|
||||
throw new DatabaseException('You cannot use RawSql for constraint in SQLite.');
|
||||
// @codeCoverageIgnore
|
||||
}
|
||||
|
||||
if (is_string(current(array_keys($constraints)))) {
|
||||
$concat1 = implode(' || ', array_keys($constraints));
|
||||
$concat2 = implode(' || ', array_values($constraints));
|
||||
} else {
|
||||
$concat1 = implode(' || ', $constraints);
|
||||
$concat2 = $concat1;
|
||||
}
|
||||
|
||||
$sql .= "WHERE {$concat1} IN (SELECT {$concat2} FROM (\n{:_table_:}))";
|
||||
|
||||
// where is not supported
|
||||
if ($this->QBWhere !== [] && $this->db->DBDebug) {
|
||||
throw new DatabaseException('You cannot use WHERE with SQLite.');
|
||||
// @codeCoverageIgnore
|
||||
}
|
||||
|
||||
$this->QBOptions['sql'] = $sql;
|
||||
}
|
||||
|
||||
if (isset($this->QBOptions['setQueryAsData'])) {
|
||||
$data = $this->QBOptions['setQueryAsData'];
|
||||
} else {
|
||||
$data = implode(
|
||||
" UNION ALL\n",
|
||||
array_map(
|
||||
static fn ($value) => 'SELECT ' . implode(', ', array_map(
|
||||
static fn ($key, $index) => $index . ' ' . $key,
|
||||
$keys,
|
||||
$value
|
||||
)),
|
||||
$values
|
||||
)
|
||||
) . "\n";
|
||||
}
|
||||
|
||||
return str_replace('{:_table_:}', $data, $sql);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use Exception;
|
||||
use SQLite3;
|
||||
use SQLite3Result;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Connection for SQLite3
|
||||
*
|
||||
* @extends BaseConnection<SQLite3, SQLite3Result>
|
||||
*/
|
||||
class Connection extends BaseConnection
|
||||
{
|
||||
/**
|
||||
* Database driver
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $DBDriver = 'SQLite3';
|
||||
|
||||
/**
|
||||
* Identifier escape character
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $escapeChar = '`';
|
||||
|
||||
/**
|
||||
* @var bool Enable Foreign Key constraint or not
|
||||
*/
|
||||
protected $foreignKeys = false;
|
||||
|
||||
/**
|
||||
* The milliseconds to sleep
|
||||
*
|
||||
* @var int|null milliseconds
|
||||
*
|
||||
* @see https://www.php.net/manual/en/sqlite3.busytimeout
|
||||
*/
|
||||
protected $busyTimeout;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function initialize()
|
||||
{
|
||||
parent::initialize();
|
||||
|
||||
if ($this->foreignKeys) {
|
||||
$this->enableForeignKeyChecks();
|
||||
}
|
||||
|
||||
if (is_int($this->busyTimeout)) {
|
||||
$this->connID->busyTimeout($this->busyTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to the database.
|
||||
*
|
||||
* @return SQLite3
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function connect(bool $persistent = false)
|
||||
{
|
||||
if ($persistent && $this->DBDebug) {
|
||||
throw new DatabaseException('SQLite3 doesn\'t support persistent connections.');
|
||||
}
|
||||
|
||||
try {
|
||||
if ($this->database !== ':memory:' && ! str_contains($this->database, DIRECTORY_SEPARATOR)) {
|
||||
$this->database = WRITEPATH . $this->database;
|
||||
}
|
||||
|
||||
$sqlite = (! $this->password)
|
||||
? new SQLite3($this->database)
|
||||
: new SQLite3($this->database, SQLITE3_OPEN_READWRITE | SQLITE3_OPEN_CREATE, $this->password);
|
||||
|
||||
$sqlite->enableExceptions(true);
|
||||
|
||||
return $sqlite;
|
||||
} catch (Exception $e) {
|
||||
throw new DatabaseException('SQLite3 error: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep or establish the connection if no queries have been sent for
|
||||
* a length of time exceeding the server's idle timeout.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reconnect()
|
||||
{
|
||||
$this->close();
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function _close()
|
||||
{
|
||||
$this->connID->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific database table to use.
|
||||
*/
|
||||
public function setDatabase(string $databaseName): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string containing the version of the database being used.
|
||||
*/
|
||||
public function getVersion(): string
|
||||
{
|
||||
if (isset($this->dataCache['version'])) {
|
||||
return $this->dataCache['version'];
|
||||
}
|
||||
|
||||
$version = SQLite3::version();
|
||||
|
||||
return $this->dataCache['version'] = $version['versionString'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the query
|
||||
*
|
||||
* @return false|SQLite3Result
|
||||
*/
|
||||
protected function execute(string $sql)
|
||||
{
|
||||
try {
|
||||
return $this->isWriteType($sql)
|
||||
? $this->connID->exec($sql)
|
||||
: $this->connID->query($sql);
|
||||
} catch (Exception $e) {
|
||||
log_message('error', (string) $e);
|
||||
|
||||
if ($this->DBDebug) {
|
||||
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the total number of rows affected by this query.
|
||||
*/
|
||||
public function affectedRows(): int
|
||||
{
|
||||
return $this->connID->changes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Platform-dependant string escape
|
||||
*/
|
||||
protected function _escapeString(string $str): string
|
||||
{
|
||||
if (! $this->connID instanceof SQLite3) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
return $this->connID->escapeString($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the SQL for listing tables in a platform-dependent manner.
|
||||
*
|
||||
* @param string|null $tableName If $tableName is provided will return only this table if exists.
|
||||
*/
|
||||
protected function _listTables(bool $prefixLimit = false, ?string $tableName = null): string
|
||||
{
|
||||
if ($tableName !== null) {
|
||||
return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
|
||||
. ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
|
||||
. ' AND "NAME" LIKE ' . $this->escape($tableName);
|
||||
}
|
||||
|
||||
return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\''
|
||||
. ' AND "NAME" NOT LIKE \'sqlite!_%\' ESCAPE \'!\''
|
||||
. (($prefixLimit !== false && $this->DBPrefix !== '')
|
||||
? ' AND "NAME" LIKE \'' . $this->escapeLikeString($this->DBPrefix) . '%\' ' . sprintf($this->likeEscapeStr, $this->likeEscapeChar)
|
||||
: '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a platform-specific query string so that the column names can be fetched.
|
||||
*/
|
||||
protected function _listColumns(string $table = ''): string
|
||||
{
|
||||
return 'PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return false|list<string>
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function getFieldNames(string $table)
|
||||
{
|
||||
// Is there a cached result?
|
||||
if (isset($this->dataCache['field_names'][$table])) {
|
||||
return $this->dataCache['field_names'][$table];
|
||||
}
|
||||
|
||||
if (! $this->connID instanceof SQLite3) {
|
||||
$this->initialize();
|
||||
}
|
||||
|
||||
$sql = $this->_listColumns($table);
|
||||
|
||||
$query = $this->query($sql);
|
||||
$this->dataCache['field_names'][$table] = [];
|
||||
|
||||
foreach ($query->getResultArray() as $row) {
|
||||
// Do we know from where to get the column's name?
|
||||
if (! isset($key)) {
|
||||
if (isset($row['column_name'])) {
|
||||
$key = 'column_name';
|
||||
} elseif (isset($row['COLUMN_NAME'])) {
|
||||
$key = 'COLUMN_NAME';
|
||||
} elseif (isset($row['name'])) {
|
||||
$key = 'name';
|
||||
} else {
|
||||
// We have no other choice but to just get the first element's key.
|
||||
$key = key($row);
|
||||
}
|
||||
}
|
||||
|
||||
$this->dataCache['field_names'][$table][] = $row[$key];
|
||||
}
|
||||
|
||||
return $this->dataCache['field_names'][$table];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with field data
|
||||
*
|
||||
* @return list<stdClass>
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
protected function _fieldData(string $table): array
|
||||
{
|
||||
if (false === $query = $this->query('PRAGMA TABLE_INFO(' . $this->protectIdentifiers($table, true, null, false) . ')')) {
|
||||
throw new DatabaseException(lang('Database.failGetFieldData'));
|
||||
}
|
||||
|
||||
$query = $query->getResultObject();
|
||||
|
||||
if (empty($query)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$retVal = [];
|
||||
|
||||
for ($i = 0, $c = count($query); $i < $c; $i++) {
|
||||
$retVal[$i] = new stdClass();
|
||||
|
||||
$retVal[$i]->name = $query[$i]->name;
|
||||
$retVal[$i]->type = $query[$i]->type;
|
||||
$retVal[$i]->max_length = null;
|
||||
$retVal[$i]->nullable = isset($query[$i]->notnull) && ! (bool) $query[$i]->notnull;
|
||||
$retVal[$i]->default = $query[$i]->dflt_value;
|
||||
// "pk" (either zero for columns that are not part of the primary key,
|
||||
// or the 1-based index of the column within the primary key).
|
||||
// https://www.sqlite.org/pragma.html#pragma_table_info
|
||||
$retVal[$i]->primary_key = ($query[$i]->pk === 0) ? 0 : 1;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with index data
|
||||
*
|
||||
* @return array<string, stdClass>
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
protected function _indexData(string $table): array
|
||||
{
|
||||
$sql = "SELECT 'PRIMARY' as indexname, l.name as fieldname, 'PRIMARY' as indextype
|
||||
FROM pragma_table_info(" . $this->escape(strtolower($table)) . ") as l
|
||||
WHERE l.pk <> 0
|
||||
UNION ALL
|
||||
SELECT sqlite_master.name as indexname, ii.name as fieldname,
|
||||
CASE
|
||||
WHEN ti.pk <> 0 AND sqlite_master.name LIKE 'sqlite_autoindex_%' THEN 'PRIMARY'
|
||||
WHEN sqlite_master.name LIKE 'sqlite_autoindex_%' THEN 'UNIQUE'
|
||||
WHEN sqlite_master.sql LIKE '% UNIQUE %' THEN 'UNIQUE'
|
||||
ELSE 'INDEX'
|
||||
END as indextype
|
||||
FROM sqlite_master
|
||||
INNER JOIN pragma_index_xinfo(sqlite_master.name) ii ON ii.name IS NOT NULL
|
||||
LEFT JOIN pragma_table_info(" . $this->escape(strtolower($table)) . ") ti ON ti.name = ii.name
|
||||
WHERE sqlite_master.type='index' AND sqlite_master.tbl_name = " . $this->escape(strtolower($table)) . ' COLLATE NOCASE';
|
||||
|
||||
if (($query = $this->query($sql)) === false) {
|
||||
throw new DatabaseException(lang('Database.failGetIndexData'));
|
||||
}
|
||||
$query = $query->getResultObject();
|
||||
|
||||
$tempVal = [];
|
||||
|
||||
foreach ($query as $row) {
|
||||
if ($row->indextype === 'PRIMARY') {
|
||||
$tempVal['PRIMARY']['indextype'] = $row->indextype;
|
||||
$tempVal['PRIMARY']['indexname'] = $row->indexname;
|
||||
$tempVal['PRIMARY']['fields'][$row->fieldname] = $row->fieldname;
|
||||
} else {
|
||||
$tempVal[$row->indexname]['indextype'] = $row->indextype;
|
||||
$tempVal[$row->indexname]['indexname'] = $row->indexname;
|
||||
$tempVal[$row->indexname]['fields'][$row->fieldname] = $row->fieldname;
|
||||
}
|
||||
}
|
||||
|
||||
$retVal = [];
|
||||
|
||||
foreach ($tempVal as $val) {
|
||||
$obj = new stdClass();
|
||||
$obj->name = $val['indexname'];
|
||||
$obj->fields = array_values($val['fields']);
|
||||
$obj->type = $val['indextype'];
|
||||
$retVal[$obj->name] = $obj;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of objects with Foreign key data
|
||||
*
|
||||
* @return array<string, stdClass>
|
||||
*/
|
||||
protected function _foreignKeyData(string $table): array
|
||||
{
|
||||
if ($this->supportsForeignKeys() !== true) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = $this->query("PRAGMA foreign_key_list({$table})")->getResult();
|
||||
$indexes = [];
|
||||
|
||||
foreach ($query as $row) {
|
||||
$indexes[$row->id]['constraint_name'] = null;
|
||||
$indexes[$row->id]['table_name'] = $table;
|
||||
$indexes[$row->id]['foreign_table_name'] = $row->table;
|
||||
$indexes[$row->id]['column_name'][] = $row->from;
|
||||
$indexes[$row->id]['foreign_column_name'][] = $row->to;
|
||||
$indexes[$row->id]['on_delete'] = $row->on_delete;
|
||||
$indexes[$row->id]['on_update'] = $row->on_update;
|
||||
$indexes[$row->id]['match'] = $row->match;
|
||||
}
|
||||
|
||||
return $this->foreignKeyDataToObjects($indexes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform-specific SQL to disable foreign key checks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _disableForeignKeyChecks()
|
||||
{
|
||||
return 'PRAGMA foreign_keys = OFF';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns platform-specific SQL to enable foreign key checks.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function _enableForeignKeyChecks()
|
||||
{
|
||||
return 'PRAGMA foreign_keys = ON';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last error code and message.
|
||||
* Must return this format: ['code' => string|int, 'message' => string]
|
||||
* intval(code) === 0 means "no error".
|
||||
*
|
||||
* @return array<string, int|string>
|
||||
*/
|
||||
public function error(): array
|
||||
{
|
||||
return [
|
||||
'code' => $this->connID->lastErrorCode(),
|
||||
'message' => $this->connID->lastErrorMsg(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert ID
|
||||
*/
|
||||
public function insertID(): int
|
||||
{
|
||||
return $this->connID->lastInsertRowID();
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin Transaction
|
||||
*/
|
||||
protected function _transBegin(): bool
|
||||
{
|
||||
return $this->connID->exec('BEGIN TRANSACTION');
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit Transaction
|
||||
*/
|
||||
protected function _transCommit(): bool
|
||||
{
|
||||
return $this->connID->exec('END TRANSACTION');
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback Transaction
|
||||
*/
|
||||
protected function _transRollback(): bool
|
||||
{
|
||||
return $this->connID->exec('ROLLBACK');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see if the current install supports Foreign Keys
|
||||
* and has them enabled.
|
||||
*/
|
||||
public function supportsForeignKeys(): bool
|
||||
{
|
||||
$result = $this->simpleQuery('PRAGMA foreign_keys');
|
||||
|
||||
return (bool) $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Database\Forge as BaseForge;
|
||||
|
||||
/**
|
||||
* Forge for SQLite3
|
||||
*/
|
||||
class Forge extends BaseForge
|
||||
{
|
||||
/**
|
||||
* DROP INDEX statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $dropIndexStr = 'DROP INDEX %s';
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* UNSIGNED support
|
||||
*
|
||||
* @var array|bool
|
||||
*/
|
||||
protected $_unsigned = false;
|
||||
|
||||
/**
|
||||
* NULL value representation in CREATE/ALTER TABLE statements
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected $null = 'NULL';
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct(BaseConnection $db)
|
||||
{
|
||||
parent::__construct($db);
|
||||
|
||||
if (version_compare($this->db->getVersion(), '3.3', '<')) {
|
||||
$this->dropTableIfStr = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create database
|
||||
*
|
||||
* @param bool $ifNotExists Whether to add IF NOT EXISTS condition
|
||||
*/
|
||||
public function createDatabase(string $dbName, bool $ifNotExists = false): bool
|
||||
{
|
||||
// In SQLite, a database is created when you connect to the database.
|
||||
// We'll return TRUE so that an error isn't generated.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop database
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function dropDatabase(string $dbName): bool
|
||||
{
|
||||
// In SQLite, a database is dropped when we delete a file
|
||||
if (! is_file($dbName)) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unable to drop the specified database.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// We need to close the pseudo-connection first
|
||||
$this->db->close();
|
||||
if (! @unlink($dbName)) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException('Unable to drop the specified database.');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (! empty($this->db->dataCache['db_names'])) {
|
||||
$key = array_search(strtolower($dbName), array_map(strtolower(...), $this->db->dataCache['db_names']), true);
|
||||
if ($key !== false) {
|
||||
unset($this->db->dataCache['db_names'][$key]);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array|string $processedFields Processed column definitions
|
||||
* or column names to DROP
|
||||
*
|
||||
* @return array|string|null
|
||||
* @return list<string>|string|null SQL string or null
|
||||
* @phpstan-return ($alterType is 'DROP' ? string : list<string>|null)
|
||||
*/
|
||||
protected function _alterTable(string $alterType, string $table, $processedFields)
|
||||
{
|
||||
switch ($alterType) {
|
||||
case 'DROP':
|
||||
$columnNamesToDrop = $processedFields;
|
||||
|
||||
$sqlTable = new Table($this->db, $this);
|
||||
|
||||
$sqlTable->fromTable($table)
|
||||
->dropColumn($columnNamesToDrop)
|
||||
->run();
|
||||
|
||||
return ''; // Why empty string?
|
||||
|
||||
case 'CHANGE':
|
||||
$fieldsToModify = [];
|
||||
|
||||
foreach ($processedFields as $processedField) {
|
||||
$name = $processedField['name'];
|
||||
$newName = $processedField['new_name'];
|
||||
|
||||
$field = $this->fields[$name];
|
||||
$field['name'] = $name;
|
||||
$field['new_name'] = $newName;
|
||||
|
||||
// Unlike when creating a table, if `null` is not specified,
|
||||
// the column will be `NULL`, not `NOT NULL`.
|
||||
if ($processedField['null'] === '') {
|
||||
$field['null'] = true;
|
||||
}
|
||||
|
||||
$fieldsToModify[] = $field;
|
||||
}
|
||||
|
||||
(new Table($this->db, $this))
|
||||
->fromTable($table)
|
||||
->modifyColumn($fieldsToModify)
|
||||
->run();
|
||||
|
||||
return null; // Why null?
|
||||
|
||||
default:
|
||||
return parent::_alterTable($alterType, $table, $processedFields);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process column
|
||||
*/
|
||||
protected function _processColumn(array $processedField): string
|
||||
{
|
||||
if ($processedField['type'] === 'TEXT' && str_starts_with($processedField['length'], "('")) {
|
||||
$processedField['type'] .= ' CHECK(' . $this->db->escapeIdentifiers($processedField['name'])
|
||||
. ' IN ' . $processedField['length'] . ')';
|
||||
}
|
||||
|
||||
return $this->db->escapeIdentifiers($processedField['name'])
|
||||
. ' ' . $processedField['type']
|
||||
. $processedField['auto_increment']
|
||||
. $processedField['null']
|
||||
. $processedField['unique']
|
||||
. $processedField['default'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Field attribute TYPE
|
||||
*
|
||||
* Performs a data type mapping between different databases.
|
||||
*/
|
||||
protected function _attributeType(array &$attributes)
|
||||
{
|
||||
switch (strtoupper($attributes['TYPE'])) {
|
||||
case 'ENUM':
|
||||
case 'SET':
|
||||
$attributes['TYPE'] = 'TEXT';
|
||||
break;
|
||||
|
||||
case 'BOOLEAN':
|
||||
$attributes['TYPE'] = 'INT';
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Field attribute AUTO_INCREMENT
|
||||
*/
|
||||
protected function _attributeAutoIncrement(array &$attributes, array &$field)
|
||||
{
|
||||
if (
|
||||
! empty($attributes['AUTO_INCREMENT'])
|
||||
&& $attributes['AUTO_INCREMENT'] === true
|
||||
&& stripos($field['type'], 'int') !== false
|
||||
) {
|
||||
$field['type'] = 'INTEGER PRIMARY KEY';
|
||||
$field['default'] = '';
|
||||
$field['null'] = '';
|
||||
$field['unique'] = '';
|
||||
$field['auto_increment'] = ' AUTOINCREMENT';
|
||||
|
||||
$this->primaryKeys = [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Foreign Key Drop
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function dropForeignKey(string $table, string $foreignName): bool
|
||||
{
|
||||
// If this version of SQLite doesn't support it, we're done here
|
||||
if ($this->db->supportsForeignKeys() !== true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise we have to copy the table and recreate
|
||||
// without the foreign key being involved now
|
||||
$sqlTable = new Table($this->db, $this);
|
||||
|
||||
return $sqlTable->fromTable($this->db->DBPrefix . $table)
|
||||
->dropForeignKey($foreignName)
|
||||
->run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop Primary Key
|
||||
*/
|
||||
public function dropPrimaryKey(string $table, string $keyName = ''): bool
|
||||
{
|
||||
$sqlTable = new Table($this->db, $this);
|
||||
|
||||
return $sqlTable->fromTable($this->db->DBPrefix . $table)
|
||||
->dropPrimaryKey()
|
||||
->run();
|
||||
}
|
||||
|
||||
public function addForeignKey($fieldName = '', string $tableName = '', $tableField = '', string $onUpdate = '', string $onDelete = '', string $fkName = ''): BaseForge
|
||||
{
|
||||
if ($fkName === '') {
|
||||
return parent::addForeignKey($fieldName, $tableName, $tableField, $onUpdate, $onDelete, $fkName);
|
||||
}
|
||||
|
||||
throw new DatabaseException('SQLite does not support foreign key names. CodeIgniter will refer to them in the format: prefix_table_column_referencecolumn_foreign');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates SQL to add primary key
|
||||
*
|
||||
* @param bool $asQuery When true recreates table with key, else partial SQL used with CREATE TABLE
|
||||
*/
|
||||
protected function _processPrimaryKeys(string $table, bool $asQuery = false): string
|
||||
{
|
||||
if ($asQuery === false) {
|
||||
return parent::_processPrimaryKeys($table, $asQuery);
|
||||
}
|
||||
|
||||
$sqlTable = new Table($this->db, $this);
|
||||
|
||||
$sqlTable->fromTable($this->db->DBPrefix . $table)
|
||||
->addPrimaryKey($this->primaryKeys)
|
||||
->run();
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates SQL to add foreign keys
|
||||
*
|
||||
* @param bool $asQuery When true recreates table with key, else partial SQL used with CREATE TABLE
|
||||
*/
|
||||
protected function _processForeignKeys(string $table, bool $asQuery = false): array
|
||||
{
|
||||
if ($asQuery === false) {
|
||||
return parent::_processForeignKeys($table, $asQuery);
|
||||
}
|
||||
|
||||
$errorNames = [];
|
||||
|
||||
foreach ($this->foreignKeys as $name) {
|
||||
foreach ($name['field'] as $f) {
|
||||
if (! isset($this->fields[$f])) {
|
||||
$errorNames[] = $f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($errorNames !== []) {
|
||||
$errorNames = [implode(', ', $errorNames)];
|
||||
|
||||
throw new DatabaseException(lang('Database.fieldNotExists', $errorNames));
|
||||
}
|
||||
|
||||
$sqlTable = new Table($this->db, $this);
|
||||
|
||||
$sqlTable->fromTable($this->db->DBPrefix . $table)
|
||||
->addForeignKey($this->foreignKeys)
|
||||
->run();
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use BadMethodCallException;
|
||||
use CodeIgniter\Database\BasePreparedQuery;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use Exception;
|
||||
use SQLite3;
|
||||
use SQLite3Result;
|
||||
use SQLite3Stmt;
|
||||
|
||||
/**
|
||||
* Prepared query for SQLite3
|
||||
*
|
||||
* @extends BasePreparedQuery<SQLite3, SQLite3Stmt, SQLite3Result>
|
||||
*/
|
||||
class PreparedQuery extends BasePreparedQuery
|
||||
{
|
||||
/**
|
||||
* The SQLite3Result resource, or false.
|
||||
*
|
||||
* @var false|SQLite3Result
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* Prepares the query against the database, and saves the connection
|
||||
* info necessary to execute the query later.
|
||||
*
|
||||
* NOTE: This version is based on SQL code. Child classes should
|
||||
* override this method.
|
||||
*
|
||||
* @param array $options Passed to the connection's prepare statement.
|
||||
* Unused in the MySQLi driver.
|
||||
*/
|
||||
public function _prepare(string $sql, array $options = []): PreparedQuery
|
||||
{
|
||||
if (! ($this->statement = $this->db->connID->prepare($sql))) {
|
||||
$this->errorCode = $this->db->connID->lastErrorCode();
|
||||
$this->errorString = $this->db->connID->lastErrorMsg();
|
||||
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException($this->errorString . ' code: ' . $this->errorCode);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a new set of data and runs it against the currently
|
||||
* prepared query. Upon success, will return a Results object.
|
||||
*/
|
||||
public function _execute(array $data): bool
|
||||
{
|
||||
if (! isset($this->statement)) {
|
||||
throw new BadMethodCallException('You must call prepare before trying to execute a prepared statement.');
|
||||
}
|
||||
|
||||
foreach ($data as $key => $item) {
|
||||
// Determine the type string
|
||||
if (is_int($item)) {
|
||||
$bindType = SQLITE3_INTEGER;
|
||||
} elseif (is_float($item)) {
|
||||
$bindType = SQLITE3_FLOAT;
|
||||
} else {
|
||||
$bindType = SQLITE3_TEXT;
|
||||
}
|
||||
|
||||
// Bind it
|
||||
$this->statement->bindValue($key + 1, $item, $bindType);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->result = $this->statement->execute();
|
||||
} catch (Exception $e) {
|
||||
if ($this->db->DBDebug) {
|
||||
throw new DatabaseException($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->result !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result object for the prepared query or false on failure.
|
||||
*
|
||||
* @return false|SQLite3Result
|
||||
*/
|
||||
public function _getResult()
|
||||
{
|
||||
return $this->result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deallocate prepared statements.
|
||||
*/
|
||||
protected function _close(): bool
|
||||
{
|
||||
return $this->statement->close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use Closure;
|
||||
use CodeIgniter\Database\BaseResult;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
use CodeIgniter\Entity\Entity;
|
||||
use SQLite3;
|
||||
use SQLite3Result;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Result for SQLite3
|
||||
*
|
||||
* @extends BaseResult<SQLite3, SQLite3Result>
|
||||
*/
|
||||
class Result extends BaseResult
|
||||
{
|
||||
/**
|
||||
* Gets the number of fields in the result set.
|
||||
*/
|
||||
public function getFieldCount(): int
|
||||
{
|
||||
return $this->resultID->numColumns();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of column names in the result set.
|
||||
*/
|
||||
public function getFieldNames(): array
|
||||
{
|
||||
$fieldNames = [];
|
||||
|
||||
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
|
||||
$fieldNames[] = $this->resultID->columnName($i);
|
||||
}
|
||||
|
||||
return $fieldNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an array of objects representing field meta-data.
|
||||
*/
|
||||
public function getFieldData(): array
|
||||
{
|
||||
static $dataTypes = [
|
||||
SQLITE3_INTEGER => 'integer',
|
||||
SQLITE3_FLOAT => 'float',
|
||||
SQLITE3_TEXT => 'text',
|
||||
SQLITE3_BLOB => 'blob',
|
||||
SQLITE3_NULL => 'null',
|
||||
];
|
||||
|
||||
$retVal = [];
|
||||
$this->resultID->fetchArray(SQLITE3_NUM);
|
||||
|
||||
for ($i = 0, $c = $this->getFieldCount(); $i < $c; $i++) {
|
||||
$retVal[$i] = new stdClass();
|
||||
$retVal[$i]->name = $this->resultID->columnName($i);
|
||||
$type = $this->resultID->columnType($i);
|
||||
$retVal[$i]->type = $type;
|
||||
$retVal[$i]->type_name = $dataTypes[$type] ?? null;
|
||||
$retVal[$i]->max_length = null;
|
||||
$retVal[$i]->length = null;
|
||||
}
|
||||
$this->resultID->reset();
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Frees the current result.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function freeResult()
|
||||
{
|
||||
if (is_object($this->resultID)) {
|
||||
$this->resultID->finalize();
|
||||
$this->resultID = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves the internal pointer to the desired offset. This is called
|
||||
* internally before fetching results to make sure the result set
|
||||
* starts at zero.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @throws DatabaseException
|
||||
*/
|
||||
public function dataSeek(int $n = 0)
|
||||
{
|
||||
if ($n !== 0) {
|
||||
throw new DatabaseException('SQLite3 doesn\'t support seeking to other offset.');
|
||||
}
|
||||
|
||||
return $this->resultID->reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an array.
|
||||
*
|
||||
* Overridden by driver classes.
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
protected function fetchAssoc()
|
||||
{
|
||||
return $this->resultID->fetchArray(SQLITE3_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the result set as an object.
|
||||
*
|
||||
* Overridden by child classes.
|
||||
*
|
||||
* @return Entity|false|object|stdClass
|
||||
*/
|
||||
protected function fetchObject(string $className = 'stdClass')
|
||||
{
|
||||
// No native support for fetching rows as objects
|
||||
if (($row = $this->fetchAssoc()) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($className === 'stdClass') {
|
||||
return (object) $row;
|
||||
}
|
||||
|
||||
$classObj = new $className();
|
||||
|
||||
if (is_subclass_of($className, Entity::class)) {
|
||||
return $classObj->injectRawData($row);
|
||||
}
|
||||
|
||||
$classSet = Closure::bind(function ($key, $value): void {
|
||||
$this->{$key} = $value;
|
||||
}, $classObj, $className);
|
||||
|
||||
foreach (array_keys($row) as $key) {
|
||||
$classSet($key, $row[$key]);
|
||||
}
|
||||
|
||||
return $classObj;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\Exceptions\DataException;
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Class Table
|
||||
*
|
||||
* Provides missing features for altering tables that are common
|
||||
* in other supported databases, but are missing from SQLite.
|
||||
* These are needed in order to support migrations during testing
|
||||
* when another database is used as the primary engine, but
|
||||
* SQLite in memory databases are used for faster test execution.
|
||||
*/
|
||||
class Table
|
||||
{
|
||||
/**
|
||||
* All of the fields this table represents.
|
||||
*
|
||||
* @var array<string, array<string, bool|int|string|null>> [name => attributes]
|
||||
*/
|
||||
protected $fields = [];
|
||||
|
||||
/**
|
||||
* All of the unique/primary keys in the table.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $keys = [];
|
||||
|
||||
/**
|
||||
* All of the foreign keys in the table.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $foreignKeys = [];
|
||||
|
||||
/**
|
||||
* The name of the table we're working with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tableName;
|
||||
|
||||
/**
|
||||
* The name of the table, with database prefix
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $prefixedTableName;
|
||||
|
||||
/**
|
||||
* Database connection.
|
||||
*
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* Handle to our forge.
|
||||
*
|
||||
* @var Forge
|
||||
*/
|
||||
protected $forge;
|
||||
|
||||
/**
|
||||
* Table constructor.
|
||||
*/
|
||||
public function __construct(Connection $db, Forge $forge)
|
||||
{
|
||||
$this->db = $db;
|
||||
$this->forge = $forge;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads an existing database table and
|
||||
* collects all of the information needed to
|
||||
* recreate this table.
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function fromTable(string $table)
|
||||
{
|
||||
$this->prefixedTableName = $table;
|
||||
|
||||
$prefix = $this->db->DBPrefix;
|
||||
|
||||
if (! empty($prefix) && str_starts_with($table, $prefix)) {
|
||||
$table = substr($table, strlen($prefix));
|
||||
}
|
||||
|
||||
if (! $this->db->tableExists($this->prefixedTableName)) {
|
||||
throw DataException::forTableNotFound($this->prefixedTableName);
|
||||
}
|
||||
|
||||
$this->tableName = $table;
|
||||
|
||||
$this->fields = $this->formatFields($this->db->getFieldData($table));
|
||||
|
||||
$this->keys = array_merge($this->keys, $this->formatKeys($this->db->getIndexData($table)));
|
||||
|
||||
// if primary key index exists twice then remove psuedo index name 'primary'.
|
||||
$primaryIndexes = array_filter($this->keys, static fn ($index) => $index['type'] === 'primary');
|
||||
|
||||
if ($primaryIndexes !== [] && count($primaryIndexes) > 1 && array_key_exists('primary', $this->keys)) {
|
||||
unset($this->keys['primary']);
|
||||
}
|
||||
|
||||
$this->foreignKeys = $this->db->getForeignKeyData($table);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after `fromTable` and any actions, like `dropColumn`, etc,
|
||||
* to finalize the action. It creates a temp table, creates the new
|
||||
* table with modifications, and copies the data over to the new table.
|
||||
* Resets the connection dataCache to be sure changes are collected.
|
||||
*/
|
||||
public function run(): bool
|
||||
{
|
||||
$this->db->query('PRAGMA foreign_keys = OFF');
|
||||
|
||||
$this->db->transStart();
|
||||
|
||||
$this->forge->renameTable($this->tableName, "temp_{$this->tableName}");
|
||||
|
||||
$this->forge->reset();
|
||||
|
||||
$this->createTable();
|
||||
|
||||
$this->copyData();
|
||||
|
||||
$this->forge->dropTable("temp_{$this->tableName}");
|
||||
|
||||
$success = $this->db->transComplete();
|
||||
|
||||
$this->db->query('PRAGMA foreign_keys = ON');
|
||||
|
||||
$this->db->resetDataCache();
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops columns from the table.
|
||||
*
|
||||
* @param list<string>|string $columns Column names to drop.
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function dropColumn($columns)
|
||||
{
|
||||
if (is_string($columns)) {
|
||||
$columns = explode(',', $columns);
|
||||
}
|
||||
|
||||
foreach ($columns as $column) {
|
||||
$column = trim($column);
|
||||
if (isset($this->fields[$column])) {
|
||||
unset($this->fields[$column]);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a field, including changing data type, renaming, etc.
|
||||
*
|
||||
* @param list<array<string, bool|int|string|null>> $fieldsToModify
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function modifyColumn(array $fieldsToModify)
|
||||
{
|
||||
foreach ($fieldsToModify as $field) {
|
||||
$oldName = $field['name'];
|
||||
unset($field['name']);
|
||||
|
||||
$this->fields[$oldName] = $field;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the primary key
|
||||
*/
|
||||
public function dropPrimaryKey(): Table
|
||||
{
|
||||
$primaryIndexes = array_filter($this->keys, static fn ($index) => strtolower($index['type']) === 'primary');
|
||||
|
||||
foreach (array_keys($primaryIndexes) as $key) {
|
||||
unset($this->keys[$key]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a foreign key from this table so that
|
||||
* it won't be recreated in the future.
|
||||
*
|
||||
* @return Table
|
||||
*/
|
||||
public function dropForeignKey(string $foreignName)
|
||||
{
|
||||
if (empty($this->foreignKeys)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
if (isset($this->foreignKeys[$foreignName])) {
|
||||
unset($this->foreignKeys[$foreignName]);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds primary key
|
||||
*/
|
||||
public function addPrimaryKey(array $fields): Table
|
||||
{
|
||||
$primaryIndexes = array_filter($this->keys, static fn ($index) => strtolower($index['type']) === 'primary');
|
||||
|
||||
// if primary key already exists we can't add another one
|
||||
if ($primaryIndexes !== []) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// add array to keys of fields
|
||||
$pk = [
|
||||
'fields' => $fields['fields'],
|
||||
'type' => 'primary',
|
||||
];
|
||||
|
||||
$this->keys['primary'] = $pk;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a foreign key
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addForeignKey(array $foreignKeys)
|
||||
{
|
||||
$fk = [];
|
||||
|
||||
// convert to object
|
||||
foreach ($foreignKeys as $row) {
|
||||
$obj = new stdClass();
|
||||
$obj->column_name = $row['field'];
|
||||
$obj->foreign_table_name = $row['referenceTable'];
|
||||
$obj->foreign_column_name = $row['referenceField'];
|
||||
$obj->on_delete = $row['onDelete'];
|
||||
$obj->on_update = $row['onUpdate'];
|
||||
|
||||
$fk[] = $obj;
|
||||
}
|
||||
|
||||
$this->foreignKeys = array_merge($this->foreignKeys, $fk);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the new table based on our current fields.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function createTable()
|
||||
{
|
||||
$this->dropIndexes();
|
||||
$this->db->resetDataCache();
|
||||
|
||||
// Handle any modified columns.
|
||||
$fields = [];
|
||||
|
||||
foreach ($this->fields as $name => $field) {
|
||||
if (isset($field['new_name'])) {
|
||||
$fields[$field['new_name']] = $field;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$fields[$name] = $field;
|
||||
}
|
||||
|
||||
$this->forge->addField($fields);
|
||||
|
||||
$fieldNames = array_keys($fields);
|
||||
|
||||
$this->keys = array_filter(
|
||||
$this->keys,
|
||||
static fn ($index) => count(array_intersect($index['fields'], $fieldNames)) === count($index['fields'])
|
||||
);
|
||||
|
||||
// Unique/Index keys
|
||||
if (is_array($this->keys)) {
|
||||
foreach ($this->keys as $keyName => $key) {
|
||||
switch ($key['type']) {
|
||||
case 'primary':
|
||||
$this->forge->addPrimaryKey($key['fields']);
|
||||
break;
|
||||
|
||||
case 'unique':
|
||||
$this->forge->addUniqueKey($key['fields'], $keyName);
|
||||
break;
|
||||
|
||||
case 'index':
|
||||
$this->forge->addKey($key['fields'], false, false, $keyName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->foreignKeys as $foreignKey) {
|
||||
$this->forge->addForeignKey(
|
||||
$foreignKey->column_name,
|
||||
trim($foreignKey->foreign_table_name, $this->db->DBPrefix),
|
||||
$foreignKey->foreign_column_name
|
||||
);
|
||||
}
|
||||
|
||||
return $this->forge->createTable($this->tableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies data from our old table to the new one,
|
||||
* taking care map data correctly based on any columns
|
||||
* that have been renamed.
|
||||
*/
|
||||
protected function copyData()
|
||||
{
|
||||
$exFields = [];
|
||||
$newFields = [];
|
||||
|
||||
foreach ($this->fields as $name => $details) {
|
||||
$newFields[] = $details['new_name'] ?? $name;
|
||||
$exFields[] = $name;
|
||||
}
|
||||
|
||||
$exFields = implode(
|
||||
', ',
|
||||
array_map(fn ($item) => $this->db->protectIdentifiers($item), $exFields)
|
||||
);
|
||||
$newFields = implode(
|
||||
', ',
|
||||
array_map(fn ($item) => $this->db->protectIdentifiers($item), $newFields)
|
||||
);
|
||||
|
||||
$this->db->query(
|
||||
"INSERT INTO {$this->prefixedTableName}({$newFields}) SELECT {$exFields} FROM {$this->db->DBPrefix}temp_{$this->tableName}"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts fields retrieved from the database to
|
||||
* the format needed for creating fields with Forge.
|
||||
*
|
||||
* @param array|bool $fields
|
||||
*
|
||||
* @return mixed
|
||||
* @phpstan-return ($fields is array ? array : mixed)
|
||||
*/
|
||||
protected function formatFields($fields)
|
||||
{
|
||||
if (! is_array($fields)) {
|
||||
return $fields;
|
||||
}
|
||||
|
||||
$return = [];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$return[$field->name] = [
|
||||
'type' => $field->type,
|
||||
'default' => $field->default,
|
||||
'null' => $field->nullable,
|
||||
];
|
||||
|
||||
if ($field->default === null) {
|
||||
// `null` means that the default value is not defined.
|
||||
unset($return[$field->name]['default']);
|
||||
} elseif ($field->default === 'NULL') {
|
||||
// 'NULL' means that the default value is NULL.
|
||||
$return[$field->name]['default'] = null;
|
||||
} else {
|
||||
$default = trim($field->default, "'");
|
||||
|
||||
if ($this->isIntegerType($field->type)) {
|
||||
$default = (int) $default;
|
||||
} elseif ($this->isNumericType($field->type)) {
|
||||
$default = (float) $default;
|
||||
}
|
||||
|
||||
$return[$field->name]['default'] = $default;
|
||||
}
|
||||
|
||||
if ($field->primary_key) {
|
||||
$this->keys['primary'] = [
|
||||
'fields' => [$field->name],
|
||||
'type' => 'primary',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is INTEGER type?
|
||||
*
|
||||
* @param string $type SQLite data type (case-insensitive)
|
||||
*
|
||||
* @see https://www.sqlite.org/datatype3.html
|
||||
*/
|
||||
private function isIntegerType(string $type): bool
|
||||
{
|
||||
return str_contains(strtoupper($type), 'INT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Is NUMERIC type?
|
||||
*
|
||||
* @param string $type SQLite data type (case-insensitive)
|
||||
*
|
||||
* @see https://www.sqlite.org/datatype3.html
|
||||
*/
|
||||
private function isNumericType(string $type): bool
|
||||
{
|
||||
return in_array(strtoupper($type), ['NUMERIC', 'DECIMAL'], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts keys retrieved from the database to
|
||||
* the format needed to create later.
|
||||
*
|
||||
* @param array<string, stdClass> $keys
|
||||
*
|
||||
* @return array<string, array{fields: string, type: string}>
|
||||
*/
|
||||
protected function formatKeys($keys)
|
||||
{
|
||||
$return = [];
|
||||
|
||||
foreach ($keys as $name => $key) {
|
||||
$return[strtolower($name)] = [
|
||||
'fields' => $key->fields,
|
||||
'type' => strtolower($key->type),
|
||||
];
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to drop all indexes and constraints
|
||||
* from the database for this table.
|
||||
*/
|
||||
protected function dropIndexes()
|
||||
{
|
||||
if (! is_array($this->keys) || $this->keys === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (array_keys($this->keys) as $name) {
|
||||
if ($name === 'primary') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->db->query("DROP INDEX IF EXISTS '{$name}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of CodeIgniter 4 framework.
|
||||
*
|
||||
* (c) CodeIgniter Foundation <admin@codeigniter.com>
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace CodeIgniter\Database\SQLite3;
|
||||
|
||||
use CodeIgniter\Database\BaseUtils;
|
||||
use CodeIgniter\Database\Exceptions\DatabaseException;
|
||||
|
||||
/**
|
||||
* Utils for SQLite3
|
||||
*/
|
||||
class Utils extends BaseUtils
|
||||
{
|
||||
/**
|
||||
* OPTIMIZE TABLE statement
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $optimizeTable = 'REINDEX %s';
|
||||
|
||||
/**
|
||||
* Platform dependent version of the backup function.
|
||||
*
|
||||
* @return never
|
||||
*/
|
||||
public function _backup(?array $prefs = null)
|
||||
{
|
||||
throw new DatabaseException('Unsupported feature of the database platform you are using.');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user