From dfa6338f2b812acd46a19b33521efc2387b689f1 Mon Sep 17 00:00:00 2001 From: Daan Willems Date: Tue, 1 Aug 2017 23:43:17 +0200 Subject: [PATCH 01/12] Add SQLite3 Support Add support for the SQLite3 database driver. --- system/Database/SQLite3/Builder.php | 91 +++++ system/Database/SQLite3/Connection.php | 441 ++++++++++++++++++++++ system/Database/SQLite3/Forge.php | 226 +++++++++++ system/Database/SQLite3/PreparedQuery.php | 134 +++++++ system/Database/SQLite3/Result.php | 196 ++++++++++ system/Database/SQLite3/Utils.php | 68 ++++ 6 files changed, 1156 insertions(+) create mode 100644 system/Database/SQLite3/Builder.php create mode 100644 system/Database/SQLite3/Connection.php create mode 100644 system/Database/SQLite3/Forge.php create mode 100644 system/Database/SQLite3/PreparedQuery.php create mode 100644 system/Database/SQLite3/Result.php create mode 100644 system/Database/SQLite3/Utils.php diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php new file mode 100644 index 000000000000..7817cb6f020e --- /dev/null +++ b/system/Database/SQLite3/Builder.php @@ -0,0 +1,91 @@ +db->DBDebug) + { + throw new DatabaseException('SQLite3 doesn\'t support persistent connections.'); + } + try + { + 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. + * + * @return mixed + */ + 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. + * + * @param string $databaseName + * + * @return mixed + */ + public function setDatabase(string $databaseName) + { + return false; + } + + //-------------------------------------------------------------------- + + /** + * Returns a string containing the version of the database being used. + * + * @return mixed + */ + public function getVersion() + { + if (isset($this->dataCache['version'])) + { + return $this->dataCache['version']; + } + + $version = \SQLite3::version(); + return $this->dataCache['version'] = $version['versionString']; + } + + //-------------------------------------------------------------------- + + + /** + * Execute the query + * + * @param string $sql + * + * @return mixed \SQLite3Result object or bool + */ + public function execute($sql) + { + return $this->isWriteType($sql) + ? $this->connID->exec($sql) + : $this->connID->query($sql); + } + + //-------------------------------------------------------------------- + + /** + * Returns the total number of rows affected by this query. + * + * @return mixed + */ + public function affectedRows(): int + { + return $this->connID->changes(); + } + + //-------------------------------------------------------------------- + + /** + * Platform-dependant string escape + * + * @param string $str + * @return string + */ + protected function _escapeString(string $str): string + { + return $this->connID->escapeString($str); + } + + //-------------------------------------------------------------------- + + /** + * Generates the SQL for listing tables in a platform-dependent manner. + * + * @param bool $prefixLimit + * + * @return string + */ + protected function _listTables($prefixLimit = false): string + { + return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\'' + .(($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. + * + * @param string $table + * + * @return string + */ + protected function _listColumns(string $table = ''): string + { + return 'PRAGMA TABLE_INFO('.$this->protectIdentifiers($table, TRUE, NULL, FALSE).')'; + } + + + /** + * Fetch Field Names + * + * @param string $table Table name + * + * @return array|false + * @throws DatabaseException + */ + public function getFieldNames($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(); + } + + if (FALSE === ($sql = $this->_listColumns($table))) + { + if ($this->DBDebug) + { + throw new DatabaseException('This feature is not available for the database you are using.'); + } + return false; + } + + $query = $this->query($sql); + $this->dataCache['field_names'][$table] = array(); + + 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 object with field data + * + * @param string $table + * @return array + */ + public function _fieldData(string $table) + { + + if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protectIdentifiers($table, TRUE, NULL, FALSE).')')) === FALSE) + { + return false; + } + $query = $query->getResultObject(); + if (empty($query)) + { + return false; + } + $retval = array(); + 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) ? (int) $query[$i]->pk : 0; + } + return $retval; + } + + //-------------------------------------------------------------------- + + /** + * Returns an object with index data + * + * @param string $table + * @return array + */ + public function _indexData(string $table) + { + // 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) + { + return false; + } + $query = $query->getResultObject(); + + $retval = []; + foreach ($query as $row) + { + $obj = new \stdClass(); + $obj->name = $row->name; + + // Get fields for index + $obj->fields = []; + if (($fields = $this->query('PRAGMA index_info(' . $this->escape(strtolower($row->name)) . ')')) === false) + { + return false; + } + $fields = $fields->getResultObject(); + + foreach($fields as $field) { + $obj->fields[] = $field->name; + } + + $retval[] = $obj; + } + + return $retval; + } + + //-------------------------------------------------------------------- + + /** + * Returns the last error code and message. + * + * Must return an array with keys 'code' and 'message': + * + * return ['code' => null, 'message' => null); + * + * @return array + */ + public function error(): array + { + return array('code' => $this->connID->lastErrorCode(), 'message' => $this->connID->lastErrorMsg()); + } + + //-------------------------------------------------------------------- + + /** + * Insert ID + * + * @return int + */ + public function insertID(): int + { + return $this->connID->lastInsertRowID(); + } + + //-------------------------------------------------------------------- + + /** + * Begin Transaction + * + * @return bool + */ + protected function _transBegin(): bool + { + return $this->connID->exec('BEGIN TRANSACTION'); } + + //-------------------------------------------------------------------- + + /** + * Commit Transaction + * + * @return bool + */ + protected function _transCommit(): bool + { + return $this->connID->exec('END TRANSACTION'); } + + //-------------------------------------------------------------------- + + /** + * Rollback Transaction + * + * @return bool + */ + protected function _transRollback(): bool + { + return $this->connID->exec('ROLLBACK'); } + + //-------------------------------------------------------------------- + + /** + * Determines if the statement is a write-type query or not. + * + * @return bool + */ + public function isWriteType($sql): bool + { + return (bool) preg_match( + '/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX)\s/i', $sql); + } + + //-------------------------------------------------------------------- + +} diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php new file mode 100644 index 000000000000..ec2b0d24172f --- /dev/null +++ b/system/Database/SQLite3/Forge.php @@ -0,0 +1,226 @@ +db->getVersion(), '3.3', '<')) + { + $this->createTableIfStr = FALSE; + $this->dropTableIfStr = FALSE; + } + } + + //-------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name + * + * @return bool + */ + public function createDatabase($db_name): 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 + * + * @param string $db_name + * + * @return bool + * @throws \CodeIgniter\DatabaseException + */ + public function dropDatabase($db_name): bool + { + // In SQLite, a database is dropped when we delete a file + if (!file_exists($db_name)) + { + 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($db_name)) + { + 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($db_name), array_map('strtolower', $this->db->dataCache['db_names']), true); + if ($key !== false) + { + unset($this->db->dataCache['db_names'][$key]); + } + } + + return true; + } + + //-------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @todo implement drop_column(), modify_column() + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * + * @return string|array + */ + protected function _alterTable($alter_type, $table, $field) + { + if (in_array($alter_type, ['DROP', 'CHANGE'], true)) + { + return FALSE; + } + return parent::_alterTable($alter_type, $table, $field); + } + + //-------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * @return string + */ + protected function _processColumn($field) + { + return $this->db->escapeIdentifiers($field['name']) + . ' ' . $field['type'] + . $field['auto_increment'] + . $field['null'] + . $field['unique'] + . $field['default']; + } + + //-------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * + * @return void + */ + protected function _attributeType(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) { + case 'ENUM': + case 'SET': + $attributes['TYPE'] = 'TEXT'; + return; + default: + return; + } + } + + //-------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * + * @return void + */ + protected function _attributeAutoIncrement(&$attributes, &$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 = array(); + } + } + + //-------------------------------------------------------------------- +} diff --git a/system/Database/SQLite3/PreparedQuery.php b/system/Database/SQLite3/PreparedQuery.php new file mode 100644 index 000000000000..af33a7d4d0f7 --- /dev/null +++ b/system/Database/SQLite3/PreparedQuery.php @@ -0,0 +1,134 @@ +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() + * + * @param array $data + * + * @return bool + */ + public function _execute($data) + { + if (is_null($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_integer($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; + } + + //-------------------------------------------------------------------- + +} diff --git a/system/Database/SQLite3/Result.php b/system/Database/SQLite3/Result.php new file mode 100644 index 000000000000..6afa78b676d4 --- /dev/null +++ b/system/Database/SQLite3/Result.php @@ -0,0 +1,196 @@ +resultID->numColumns(); + } + + //-------------------------------------------------------------------- + + /** + * Generates an array of column names in the result set. + * + * @return array + */ + 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. + * + * @return array + */ + public function getFieldData(): array + { + static $data_types = [ + SQLITE3_INTEGER => 'integer', + SQLITE3_FLOAT => 'float', + SQLITE3_TEXT => 'text', + SQLITE3_BLOB => 'blob', + SQLITE3_NULL => 'null' + ]; + + $retval = []; + + 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 = isset($data_types[$type]) ? $data_types[$type] : $type; + $retval[$i]->max_length = NULL; + } + + return $retval; + } + + //-------------------------------------------------------------------- + + /** + * Frees the current result. + * + * @return mixed + */ + 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. + * + * @param int $n + * + * @return mixed + * @throws \CodeIgniter\DatabaseException + */ + public function dataSeek($n = 0) + { + if ($n != 0) + { + if ($this->db->DBDebug) { + throw new DatabaseException('SQLite3 doesn\'t support seeking to other offset.'); + } + return false; + } + return $this->resultID->reset(); + } + + //-------------------------------------------------------------------- + + /** + * Returns the result set as an array. + * + * Overridden by driver classes. + * + * @return array + */ + protected function fetchAssoc() + { + return $this->resultID->fetchArray(SQLITE3_ASSOC); + } + + //-------------------------------------------------------------------- + + /** + * Returns the result set as an object. + * + * Overridden by child classes. + * + * @param string $className + * + * @return object + */ + protected function fetchObject($className = 'stdClass') + { + // No native support for fetching rows as objects + if (($row = $this->fetchAssoc()) === FALSE) + { + return FALSE; + } + elseif ($className === 'stdClass') + { + return (object) $row; + } + + $classObj = new $className(); + $classSet = \Closure::bind(function ($key, $value) { + $this->$key = $value; + }, $classObj, $className + ); + foreach (array_keys($row) as $key) + { + $classSet($key, $row[$key]); + } + return $classObj; + } + + //-------------------------------------------------------------------- +} diff --git a/system/Database/SQLite3/Utils.php b/system/Database/SQLite3/Utils.php new file mode 100644 index 000000000000..9df3b665ca47 --- /dev/null +++ b/system/Database/SQLite3/Utils.php @@ -0,0 +1,68 @@ + Date: Tue, 1 Aug 2017 23:50:44 +0200 Subject: [PATCH 02/12] Modify tests for SQLite3 SQLite3 handles auto_increment different: It remembers the old value even after removing the table. Tests fail because of this. Since users may expect this behaviour, the best choice is to change the tests to use an 'unique' instead of 'auto_increment' on SQLite3. --- .../Migrations/20160428212500_Create_test_tables.php | 11 +++++++---- tests/system/Database/Live/ForgeTest.php | 5 ++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php index fd430eafaf80..cea0a54d3ccb 100644 --- a/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php +++ b/tests/_support/Database/Migrations/20160428212500_Create_test_tables.php @@ -4,9 +4,12 @@ class Migration_Create_test_tables extends \CodeIgniter\Database\Migration { public function up() { + // SQLite3 uses auto increment different + $unique_or_auto = $this->db->DBDriver == 'SQLite3' ? 'unique' : 'auto_increment'; + // User Table $this->forge->addField([ - 'id' => ['type' => 'INTEGER', 'constraint' => 3, 'auto_increment' => true], + 'id' => ['type' => 'INTEGER', 'constraint' => 3, $unique_or_auto => true], 'name' => ['type' => 'VARCHAR', 'constraint' => 80,], 'email' => ['type' => 'VARCHAR', 'constraint' => 100], 'country' => ['type' => 'VARCHAR', 'constraint' => 40,], @@ -17,7 +20,7 @@ public function up() // Job Table $this->forge->addField([ - 'id' => ['type' => 'INTEGER', 'constraint' => 3, 'auto_increment' => true], + 'id' => ['type' => 'INTEGER', 'constraint' => 3, $unique_or_auto => true], 'name' => ['type' => 'VARCHAR', 'constraint' => 40], 'description' => ['type' => 'TEXT'], 'created_at' => ['type' => 'DATETIME', 'null' => true] @@ -27,7 +30,7 @@ public function up() // Misc Table $this->forge->addField([ - 'id' => ['type' => 'INTEGER', 'constraint' => 3, 'auto_increment' => true ], + 'id' => ['type' => 'INTEGER', 'constraint' => 3, $unique_or_auto => true], 'key' => ['type' => 'VARCHAR', 'constraint' => 40], 'value' => ['type' => 'TEXT'], ]); @@ -36,7 +39,7 @@ public function up() // Empty Table $this->forge->addField([ - 'id' => ['type' => 'INTEGER', 'constraint' => 3, 'auto_increment' => true], + 'id' => ['type' => 'INTEGER', 'constraint' => 3, $unique_or_auto => true], 'name' => ['type' => 'VARCHAR', 'constraint' => 40,], ]); $this->forge->addKey('id', true); diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 33f9a23661ed..ec476a97f940 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -17,11 +17,14 @@ public function setUp() public function testCompositeKey() { + // SQLite3 uses auto increment different + $unique_or_auto = $this->db->DBDriver == 'SQLite3' ? 'unique' : 'auto_increment'; + $this->forge->addField([ 'id' => [ 'type' => 'INTEGER', 'constraint' => 3, - 'auto_increment' => true, + $unique_or_auto => true, ], 'code' => [ 'type' => 'VARCHAR', From 179c4a14657391814fef86dcc249f8772101591c Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Mon, 23 Oct 2017 23:20:25 -0500 Subject: [PATCH 03/12] Getting SQLite foreign keys mostly working. --- application/Controllers/Checks.php | 59 +++ phpunit.xml.dist | 5 +- system/Database/BaseBuilder.php | 25 + system/Database/SQLite3/Builder.php | 16 + system/Database/SQLite3/Connection.php | 551 +++++++++++++---------- tests/system/Database/Live/ForgeTest.php | 4 +- 6 files changed, 413 insertions(+), 247 deletions(-) diff --git a/application/Controllers/Checks.php b/application/Controllers/Checks.php index 5fbd34cfb412..da6b05ddb5b0 100644 --- a/application/Controllers/Checks.php +++ b/application/Controllers/Checks.php @@ -159,6 +159,65 @@ public function forge() } + public function sqlite() + { + $forge = \Config\Database::forge(); + $forge->dropTable('users', true); + $forge->dropTable('invoices', true); + + // Ensure Foreign Keys are on + $forge->getConnection()->simpleQuery("PRAGMA foreign_keys=1"); + + // Create a table + $forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + 'auto_increment' => true, + ], + 'name' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + ] + ]); + $forge->addKey('id', true); + $forge->createTable('users', true); + + $data_insert = array( + 'id' => 1, + 'name' => 'User 1', + ); + + $forge->getConnection()->table('users')->insert($data_insert); + $forge->addField([ + 'id' => [ + 'type' => 'INTEGER', + 'constraint' => 11, + 'auto_increment' => true, + ], + 'users_id' => [ + 'type' => 'INTEGER', + 'constraint' => 11 + ], + 'other_id' => [ + 'type' => 'INTEGER', + 'constraint' => 11 + ], + 'another_id' => [ + 'type' => 'INTEGER', + 'constraint' => 11 + ] + ]); + $forge->addKey('id', true); + + $forge->addForeignKey('users_id','users','id','CASCADE','CASCADE'); + $forge->addForeignKey('other_id','users','id'); + + $res = $forge->createTable('invoices', true); + + dd($forge->getConnection()->getForeignKeyData('invoices')); + } + public function escape() { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d720c67e4040..2e29cf36713d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -12,7 +12,10 @@ ./tests/system - + ./tests/system/Database + + + ./tests/system/Database diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index c616c3995f45..f65859051e0a 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -195,6 +195,21 @@ class BaseBuilder */ protected $binds = []; + /** + * Some databases, like SQLite, do not by default + * allow limiting of delete clauses. + * + * @var bool + */ + protected $canLimitDeletes = true; + + /** + * Some databases do not by default + * allow limit update queries with WHERE. + * @var bool + */ + protected $canLimitWhereUpdates = true; + //-------------------------------------------------------------------- /** @@ -1880,6 +1895,11 @@ public function update($set = null, $where = null, int $limit = null, $test = fa if ( ! empty($limit)) { + if (! $this->canLimitWhereUpdates) + { + throw new DatabaseException('This driver does not allow LIMITs on UPDATE queries using WHERE.'); + } + $this->limit($limit); } @@ -2264,6 +2284,11 @@ public function delete($where = '', $limit = null, $reset_data = true, $returnSQ if ( ! empty($this->QBLimit)) { + if (! $this->canLimitDeletes) + { + throw new DatabaseException('SQLite3 does not allow LIMITs on DELETE queries.'); + } + $sql = $this->_limit($sql); } diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php index 7817cb6f020e..1c6f4c2eed07 100644 --- a/system/Database/SQLite3/Builder.php +++ b/system/Database/SQLite3/Builder.php @@ -50,6 +50,22 @@ class Builder extends BaseBuilder */ protected $escapeChar = '`'; + /** + * 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; + //-------------------------------------------------------------------- /** diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index f0d989926ad0..7bd3829bb3bf 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -27,17 +27,17 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * - * @package CodeIgniter - * @author CodeIgniter Dev Team - * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/) - * @license https://opensource.org/licenses/MIT MIT License - * @link https://codeigniter.com - * @since Version 3.0.0 + * @package CodeIgniter + * @author CodeIgniter Dev Team + * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/) + * @license https://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com + * @since Version 3.0.0 * @filesource */ use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\ConnectionInterface; -use CodeIgniter\DatabaseException; +use CodeIgniter\Database\Exceptions\DatabaseException; /** * Connection for SQLite3 @@ -54,68 +54,65 @@ class Connection extends BaseConnection implements ConnectionInterface // -------------------------------------------------------------------- - /** - * ORDER BY random keyword - * - * @var array - */ - protected $_random_keyword = array('RANDOM()', 'RANDOM()'); + /** + * ORDER BY random keyword + * + * @var array + */ + protected $_random_keyword = ['RANDOM()', 'RANDOM()']; //-------------------------------------------------------------------- /** * Connect to the database. * - * @todo enableExceptions - * * @param bool $persistent * * @return mixed - * @throws \CodeIgniter\DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function connect($persistent = false) { - if ($persistent and $this->db->DBDebug) - { - throw new DatabaseException('SQLite3 doesn\'t support persistent connections.'); - } - try - { - 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()); - } + if ($persistent and $this->db->DBDebug) + { + throw new DatabaseException('SQLite3 doesn\'t support persistent connections.'); + } + try + { + 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. - * - * @return mixed - */ - public function reconnect() - { - $this->close(); - $this->initialize(); - } - - //-------------------------------------------------------------------- - - /** - * Close the database connection. - * - * @return void - */ - protected function _close() - { - $this->connID->close(); - } + //-------------------------------------------------------------------- + + /** + * 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() + { + $this->close(); + $this->initialize(); + } + + //-------------------------------------------------------------------- + + /** + * Close the database connection. + * + * @return void + */ + protected function _close() + { + $this->connID->close(); + } //-------------------------------------------------------------------- @@ -145,28 +142,29 @@ public function getVersion() return $this->dataCache['version']; } - $version = \SQLite3::version(); - return $this->dataCache['version'] = $version['versionString']; + $version = \SQLite3::version(); + + return $this->dataCache['version'] = $version['versionString']; } //-------------------------------------------------------------------- - /** - * Execute the query - * - * @param string $sql - * - * @return mixed \SQLite3Result object or bool - */ - public function execute($sql) - { - return $this->isWriteType($sql) - ? $this->connID->exec($sql) - : $this->connID->query($sql); - } + /** + * Execute the query + * + * @param string $sql + * + * @return mixed \SQLite3Result object or bool + */ + public function execute($sql) + { + return $this->isWriteType($sql) + ? $this->connID->exec($sql) + : $this->connID->query($sql); + } - //-------------------------------------------------------------------- + //-------------------------------------------------------------------- /** * Returns the total number of rows affected by this query. @@ -183,12 +181,13 @@ public function affectedRows(): int /** * Platform-dependant string escape * - * @param string $str - * @return string + * @param string $str + * + * @return string */ protected function _escapeString(string $str): string { - return $this->connID->escapeString($str); + return $this->connID->escapeString($str); } //-------------------------------------------------------------------- @@ -202,165 +201,173 @@ protected function _escapeString(string $str): string */ protected function _listTables($prefixLimit = false): string { - return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\'' - .(($prefixLimit !== FALSE && $this->DBPrefix != '') - ? ' AND "NAME" LIKE \''.$this->escapeLikeString($this->DBPrefix).'%\' '.sprintf($this->likeEscapeStr, $this->likeEscapeChar) - : ''); + return 'SELECT "NAME" FROM "SQLITE_MASTER" WHERE "TYPE" = \'table\'' + .(($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. - * - * @param string $table - * - * @return string - */ - protected function _listColumns(string $table = ''): string - { - return 'PRAGMA TABLE_INFO('.$this->protectIdentifiers($table, TRUE, NULL, FALSE).')'; - } - - - /** - * Fetch Field Names - * - * @param string $table Table name - * - * @return array|false - * @throws DatabaseException - */ - public function getFieldNames($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(); - } - - if (FALSE === ($sql = $this->_listColumns($table))) - { - if ($this->DBDebug) - { - throw new DatabaseException('This feature is not available for the database you are using.'); - } - return false; - } - - $query = $this->query($sql); - $this->dataCache['field_names'][$table] = array(); - - 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]; - } - - //-------------------------------------------------------------------- - - - /** + //-------------------------------------------------------------------- + + /** + * Generates a platform-specific query string so that the column names can be fetched. + * + * @param string $table + * + * @return string + */ + protected function _listColumns(string $table = ''): string + { + return 'PRAGMA TABLE_INFO('.$this->protectIdentifiers($table, true, null, false).')'; + } + + + /** + * Fetch Field Names + * + * @param string $table Table name + * + * @return array|false + * @throws DatabaseException + */ + public function getFieldNames($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(); + } + + if (false === ($sql = $this->_listColumns($table))) + { + if ($this->DBDebug) + { + throw new DatabaseException('This feature is not available for the database you are using.'); + } + + return false; + } + + $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 object with field data * - * @param string $table - * @return array + * @param string $table + * + * @return array */ public function _fieldData(string $table) { - if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protectIdentifiers($table, TRUE, NULL, FALSE).')')) === FALSE) - { - return false; - } - $query = $query->getResultObject(); - if (empty($query)) - { - return false; - } - $retval = array(); - 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) ? (int) $query[$i]->pk : 0; - } - return $retval; + if (($query = $this->query('PRAGMA TABLE_INFO('.$this->protectIdentifiers($table, true, null, + false).')')) === false) + { + return false; + } + $query = $query->getResultObject(); + if (empty($query)) + { + return false; + } + $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) ? (int)$query[$i]->pk : 0; + } + + return $retval; } - //-------------------------------------------------------------------- - - /** - * Returns an object with index data - * - * @param string $table - * @return array - */ - public function _indexData(string $table) - { - // 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) - { - return false; - } - $query = $query->getResultObject(); - - $retval = []; - foreach ($query as $row) - { - $obj = new \stdClass(); - $obj->name = $row->name; - - // Get fields for index - $obj->fields = []; - if (($fields = $this->query('PRAGMA index_info(' . $this->escape(strtolower($row->name)) . ')')) === false) - { - return false; - } - $fields = $fields->getResultObject(); - - foreach($fields as $field) { - $obj->fields[] = $field->name; - } - - $retval[] = $obj; - } - - return $retval; - } + //-------------------------------------------------------------------- + + /** + * Returns an object with index data + * + * @param string $table + * + * @return array + */ + public function _indexData(string $table) + { + // 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) + { + return false; + } + $query = $query->getResultObject(); + + $retval = []; + foreach ($query as $row) + { + $obj = new \stdClass(); + $obj->name = $row->name; + + // Get fields for index + $obj->fields = []; + if (($fields = $this->query('PRAGMA index_info('.$this->escape(strtolower($row->name)).')')) === false) + { + return false; + } + $fields = $fields->getResultObject(); + + foreach ($fields as $field) + { + $obj->fields[] = $field->name; + } + + $retval[] = $obj; + } + + return $retval; + } //-------------------------------------------------------------------- @@ -371,11 +378,11 @@ public function _indexData(string $table) * * return ['code' => null, 'message' => null); * - * @return array + * @return array */ public function error(): array { - return array('code' => $this->connID->lastErrorCode(), 'message' => $this->connID->lastErrorMsg()); + return ['code' => $this->connID->lastErrorCode(), 'message' => $this->connID->lastErrorMsg()]; } //-------------------------------------------------------------------- @@ -383,7 +390,7 @@ public function error(): array /** * Insert ID * - * @return int + * @return int */ public function insertID(): int { @@ -395,47 +402,105 @@ public function insertID(): int /** * Begin Transaction * - * @return bool + * @return bool */ protected function _transBegin(): bool { - return $this->connID->exec('BEGIN TRANSACTION'); } + return $this->connID->exec('BEGIN TRANSACTION'); + } //-------------------------------------------------------------------- /** * Commit Transaction * - * @return bool + * @return bool */ protected function _transCommit(): bool { - return $this->connID->exec('END TRANSACTION'); } + return $this->connID->exec('END TRANSACTION'); + } //-------------------------------------------------------------------- /** * Rollback Transaction * - * @return bool + * @return bool */ protected function _transRollback(): bool { - return $this->connID->exec('ROLLBACK'); } + return $this->connID->exec('ROLLBACK'); + } - //-------------------------------------------------------------------- + //-------------------------------------------------------------------- - /** - * Determines if the statement is a write-type query or not. - * - * @return bool - */ - public function isWriteType($sql): bool - { - return (bool) preg_match( - '/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX)\s/i', $sql); - } + /** + * Determines if the statement is a write-type query or not. + * + * @return bool + */ + public function isWriteType($sql): bool + { + return (bool)preg_match( + '/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX)\s/i', + $sql); + } //-------------------------------------------------------------------- + /** + * Checks to see if the current install supports Foreign Keys + * and has them enabled. + * + * @return bool + */ + protected function supportsForeignKeys(): bool + { + $result = $this->simpleQuery("PRAGMA foreign_keys"); + + return (bool)$result; + } + + /** + * Returns an object with Foreign key data + * + * @param string $table + * @return array + */ + public function _foreignKeyData(string $table) + { + 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; + + $retval[] = $obj; + } + } + + return $retval; + } + + //-------------------------------------------------------------------- } diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 64f9c67e2eb0..f6210ece52ba 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -75,7 +75,6 @@ public function testAddFields() $this->assertTrue(in_array('name', $fieldsNames)); $this->assertTrue(in_array('active', $fieldsNames)); - $fieldsData = $this->db->getFieldData('forge_test_fields'); $this->assertTrue(in_array($fieldsData[0]->name, ['id', 'name', 'username', 'active'])); @@ -192,8 +191,7 @@ public function testForeignKey() $foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices'); - $this->assertEquals($foreignKeyData[0]->constraint_name, - $this->db->DBPrefix.'forge_test_invoices_users_id_foreign'); + $this->assertEquals($foreignKeyData[0]->constraint_name,$this->db->DBPrefix.'forge_test_invoices_users_id_foreign'); $this->assertEquals($foreignKeyData[0]->table_name, $this->db->DBPrefix.'forge_test_invoices'); $this->assertEquals($foreignKeyData[0]->foreign_table_name, $this->db->DBPrefix.'forge_test_users'); From 143acf11903151b7b857304a081d418c7461df3e Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Wed, 25 Oct 2017 22:14:37 -0500 Subject: [PATCH 04/12] Cleaning up foreign key support --- system/Database/Forge.php | 302 +++++++++-------- system/Database/SQLite3/Forge.php | 399 ++++++++++++----------- system/Language/en/Database.php | 2 + tests/system/Database/Live/ForgeTest.php | 35 +- 4 files changed, 403 insertions(+), 335 deletions(-) diff --git a/system/Database/Forge.php b/system/Database/Forge.php index ae0885784126..487587f6123a 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -27,12 +27,12 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * - * @package CodeIgniter - * @author CodeIgniter Dev Team - * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/) - * @license https://opensource.org/licenses/MIT MIT License - * @link https://codeigniter.com - * @since Version 3.0.0 + * @package CodeIgniter + * @author CodeIgniter Dev Team + * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/) + * @license https://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com + * @since Version 3.0.0 * @filesource */ use \CodeIgniter\Database\Exceptions\DatabaseException; @@ -59,6 +59,7 @@ class Forge /** * List of keys. + * * @var array */ protected $keys = []; @@ -69,8 +70,8 @@ class Forge * @var array */ protected $primaryKeys = []; - - /** + + /** * List of foreign keys. * * @var type @@ -168,7 +169,7 @@ class Forge */ public function __construct(ConnectionInterface $db) { - $this->db = & $db; + $this->db = &$db; } //-------------------------------------------------------------------- @@ -204,7 +205,7 @@ public function createDatabase($db_name) return false; } - elseif ( ! $this->db->query(sprintf($this->createDatabaseStr, $db_name, $this->db->charset, $this->db->DBCollat)) + elseif (! $this->db->query(sprintf($this->createDatabaseStr, $db_name, $this->db->charset, $this->db->DBCollat)) ) { if ($this->db->DBDebug) @@ -215,7 +216,7 @@ public function createDatabase($db_name) return false; } - if ( ! empty($this->db->dataCache['db_names'])) + if (! empty($this->db->dataCache['db_names'])) { $this->db->dataCache['db_names'][] = $db_name; } @@ -244,7 +245,7 @@ public function dropDatabase($db_name) return false; } - elseif ( ! $this->db->query(sprintf($this->dropDatabaseStr, $db_name))) + elseif (! $this->db->query(sprintf($this->dropDatabaseStr, $db_name))) { if ($this->db->DBDebug) { @@ -254,7 +255,7 @@ public function dropDatabase($db_name) return false; } - if ( ! empty($this->db->dataCache['db_names'])) + if (! empty($this->db->dataCache['db_names'])) { $key = array_search(strtolower($db_name), array_map('strtolower', $this->db->dataCache['db_names']), true); if ($key !== false) @@ -280,7 +281,7 @@ public function addKey($key, $primary = false) { if ($primary === true) { - foreach ((array) $key as $one) + foreach ((array)$key as $one) { $this->primaryKeys[] = $one; } @@ -310,8 +311,8 @@ public function addField($field) { $this->addField([ 'id' => [ - 'type' => 'INT', - 'constraint' => 9, + 'type' => 'INT', + 'constraint' => 9, 'auto_increment' => true, ], ]); @@ -337,48 +338,54 @@ public function addField($field) } //-------------------------------------------------------------------- - - /** + + /** * Add Foreign Key * - * @param array $field + * @param string $fieldName + * @param string $tableName + * @param string $tableField + * @param bool $onUpdate + * @param bool $onDelete * - * @return \CodeIgniter\Database\Forge + * @return \CodeIgniter\Database\Forge + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ - public function addForeignKey($fieldName= '',$tableName = '', $tableField = '', $onUpdate = false, $onDelete = false) + public function addForeignKey($fieldName = '', $tableName = '', $tableField = '', $onUpdate = false, $onDelete = false) { - - if( ! isset($this->fields[$fieldName])) - { - throw new \RuntimeException('Field "'.$fieldName.'" not exist'); - } - - $this->foreignKeys[$fieldName] = [ - 'table' => $tableName, - 'field' => $tableField, - 'onDelete' => $onDelete, - 'onUpdate' => $onUpdate - ]; - - - return $this; + + if (! isset($this->fields[$fieldName])) + { + throw new DatabaseException(lang('Database.fieldNotExists', [$fieldName])); + } + + $this->foreignKeys[$fieldName] = [ + 'table' => $tableName, + 'field' => $tableField, + 'onDelete' => $onDelete, + 'onUpdate' => $onUpdate, + ]; + + + return $this; } //-------------------------------------------------------------------- - - /** + + /** * Foreign Key Drop * - * @param string $table Table name + * @param string $table Table name * @param string $foreign_name Foreign name * * @return bool */ public function dropForeignKey($table, $foreign_name) { - - $sql = sprintf($this->dropConstraintStr,$this->db->escapeIdentifiers($this->db->DBPrefix.$table),$this->db->escapeIdentifiers($this->db->DBPrefix.$foreign_name)); - + + $sql = sprintf($this->dropConstraintStr, $this->db->escapeIdentifiers($this->db->DBPrefix.$table), + $this->db->escapeIdentifiers($this->db->DBPrefix.$foreign_name)); + if ($sql === false) { if ($this->db->DBDebug) @@ -393,7 +400,7 @@ public function dropForeignKey($table, $foreign_name) } //-------------------------------------------------------------------- - + /** * Create Table * @@ -412,7 +419,7 @@ public function createTable($table, $if_not_exists = false, array $attributes = } else { - $table = $this->db->DBPrefix . $table; + $table = $this->db->DBPrefix.$table; } if (count($this->fields) === 0) @@ -441,9 +448,9 @@ public function createTable($table, $if_not_exists = false, array $attributes = empty($this->db->dataCache['table_names']) OR $this->db->dataCache['table_names'][] = $table; // Most databases don't support creating indexes from within the CREATE TABLE statement - if ( ! empty($this->keys)) + if (! empty($this->keys)) { - for ($i = 0, $sqls = $this->_processIndexes($table), $c = count($sqls); $i < $c; $i ++ ) + for ($i = 0, $sqls = $this->_processIndexes($table), $c = count($sqls); $i < $c; $i++) { $this->db->query($sqls[$i]); } @@ -481,18 +488,20 @@ protected function _createTable($table, $if_not_exists, $attributes) } } - $sql = ($if_not_exists) ? sprintf($this->createTableIfStr, $this->db->escapeIdentifiers($table)) : 'CREATE TABLE'; + $sql = ($if_not_exists) ? sprintf($this->createTableIfStr, $this->db->escapeIdentifiers($table)) + : 'CREATE TABLE'; $columns = $this->_processFields(true); - for ($i = 0, $c = count($columns); $i < $c; $i ++ ) + for ($i = 0, $c = count($columns); $i < $c; $i++) { - $columns[$i] = ($columns[$i]['_literal'] !== false) ? "\n\t" . $columns[$i]['_literal'] : "\n\t" . $this->_processColumn($columns[$i]); + $columns[$i] = ($columns[$i]['_literal'] !== false) ? "\n\t".$columns[$i]['_literal'] + : "\n\t".$this->_processColumn($columns[$i]); } - $columns = implode(',', $columns); + $columns = implode(',', $columns); - $columns .= $this->_processPrimaryKeys($table); - $columns .= $this->_processForeignKeys($table); + $columns .= $this->_processPrimaryKeys($table); + $columns .= $this->_processForeignKeys($table); // Are indexes created from within the CREATE TABLE statement? (e.g. in MySQL) if ($this->createTableKeys === true) @@ -501,7 +510,8 @@ protected function _createTable($table, $if_not_exists, $attributes) } // createTableStr will usually have the following format: "%s %s (%s\n)" - $sql = sprintf($this->createTableStr . '%s', $sql, $this->db->escapeIdentifiers($table), $columns, $this->_createTableAttributes($attributes)); + $sql = sprintf($this->createTableStr.'%s', $sql, $this->db->escapeIdentifiers($table), $columns, + $this->_createTableAttributes($attributes)); return $sql; } @@ -523,7 +533,7 @@ protected function _createTableAttributes($attributes) { if (is_string($key)) { - $sql .= ' ' . strtoupper($key) . ' ' . $attributes[$key]; + $sql .= ' '.strtoupper($key).' '.$attributes[$key]; } } @@ -537,7 +547,7 @@ protected function _createTableAttributes($attributes) * * @param string $table_name Table name * @param bool $if_exists Whether to add an IF EXISTS condition - * @param bool $cascade Whether to add an CASCADE condition + * @param bool $cascade Whether to add an CASCADE condition * * @return mixed * @throws \CodeIgniter\Database\Exceptions\DatabaseException @@ -554,14 +564,14 @@ public function dropTable($table_name, $if_exists = false, $cascade = false) return false; } - + // If the prefix is already starting the table name, remove it... if (! empty($this->db->DBPrefix) && strpos($table_name, $this->db->DBPrefix) === 0) { $table_name = substr($table_name, strlen($this->db->DBPrefix)); } - if (($query = $this->_dropTable($this->db->DBPrefix . $table_name, $if_exists, $cascade)) === true) + if (($query = $this->_dropTable($this->db->DBPrefix.$table_name, $if_exists, $cascade)) === true) { return true; } @@ -571,7 +581,8 @@ public function dropTable($table_name, $if_exists = false, $cascade = false) // Update table list cache if ($query && ! empty($this->db->dataCache['table_names'])) { - $key = array_search(strtolower($this->db->DBPrefix . $table_name), array_map('strtolower', $this->db->dataCache['table_names']), true); + $key = array_search(strtolower($this->db->DBPrefix.$table_name), + array_map('strtolower', $this->db->dataCache['table_names']), true); if ($key !== false) { unset($this->db->dataCache['table_names'][$key]); @@ -590,7 +601,7 @@ public function dropTable($table_name, $if_exists = false, $cascade = false) * * @param string $table Table name * @param bool $if_exists Whether to add an IF EXISTS condition - * @param bool $cascade Whether to add an CASCADE condition + * @param bool $cascade Whether to add an CASCADE condition * * @return string */ @@ -602,7 +613,7 @@ protected function _dropTable($table, $if_exists, $cascade) { if ($this->dropTableIfStr === false) { - if ( ! $this->db->tableExists($table)) + if (! $this->db->tableExists($table)) { return true; } @@ -613,8 +624,8 @@ protected function _dropTable($table, $if_exists, $cascade) } } - $sql = $sql . ' ' . $this->db->escapeIdentifiers($table); - + $sql = $sql.' '.$this->db->escapeIdentifiers($table); + return $sql; } @@ -645,15 +656,18 @@ public function renameTable($table_name, $new_table_name) return false; } - $result = $this->db->query(sprintf($this->renameTableStr, $this->db->escapeIdentifiers($this->db->DBPrefix . $table_name), $this->db->escapeIdentifiers($this->db->DBPrefix . $new_table_name)) + $result = $this->db->query(sprintf($this->renameTableStr, + $this->db->escapeIdentifiers($this->db->DBPrefix.$table_name), + $this->db->escapeIdentifiers($this->db->DBPrefix.$new_table_name)) ); if ($result && ! empty($this->db->dataCache['table_names'])) { - $key = array_search(strtolower($this->db->DBPrefix . $table_name), array_map('strtolower', $this->db->dataCache['table_names']), true); + $key = array_search(strtolower($this->db->DBPrefix.$table_name), + array_map('strtolower', $this->db->dataCache['table_names']), true); if ($key !== false) { - $this->db->dataCache['table_names'][$key] = $this->db->DBPrefix . $new_table_name; + $this->db->dataCache['table_names'][$key] = $this->db->DBPrefix.$new_table_name; } } @@ -665,11 +679,11 @@ public function renameTable($table_name, $new_table_name) /** * Column Add * - * @param string $table Table name - * @param array $field Column definition + * @param string $table Table name + * @param array $field Column definition * * @return bool - * @throws \CodeIgniter\Database\Exceptions\DatabaseException + * @throws \CodeIgniter\Database\Exceptions\DatabaseException */ public function addColumn($table, $field) { @@ -681,7 +695,7 @@ public function addColumn($table, $field) $this->addField([$k => $field[$k]]); } - $sqls = $this->_alterTable('ADD', $this->db->DBPrefix . $table, $this->_processFields()); + $sqls = $this->_alterTable('ADD', $this->db->DBPrefix.$table, $this->_processFields()); $this->_reset(); if ($sqls === false) { @@ -693,7 +707,7 @@ public function addColumn($table, $field) return false; } - for ($i = 0, $c = count($sqls); $i < $c; $i ++ ) + for ($i = 0, $c = count($sqls); $i < $c; $i++) { if ($this->db->query($sqls[$i]) === false) { @@ -717,7 +731,7 @@ public function addColumn($table, $field) */ public function dropColumn($table, $column_name) { - $sql = $this->_alterTable('DROP', $this->db->DBPrefix . $table, $column_name); + $sql = $this->_alterTable('DROP', $this->db->DBPrefix.$table, $column_name); if ($sql === false) { if ($this->db->DBDebug) @@ -757,7 +771,7 @@ public function modifyColumn($table, $field) throw new \RuntimeException('Field information is required'); } - $sqls = $this->_alterTable('CHANGE', $this->db->DBPrefix . $table, $this->_processFields()); + $sqls = $this->_alterTable('CHANGE', $this->db->DBPrefix.$table, $this->_processFields()); $this->_reset(); if ($sqls === false) { @@ -769,7 +783,7 @@ public function modifyColumn($table, $field) return false; } - for ($i = 0, $c = count($sqls); $i < $c; $i ++ ) + for ($i = 0, $c = count($sqls); $i < $c; $i++) { if ($this->db->query($sqls[$i]) === false) { @@ -793,21 +807,21 @@ public function modifyColumn($table, $field) */ protected function _alterTable($alter_type, $table, $field) { - $sql = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table) . ' '; + $sql = 'ALTER TABLE '.$this->db->escapeIdentifiers($table).' '; // DROP has everything it needs now. if ($alter_type === 'DROP') { - return $sql . 'DROP COLUMN ' . $this->db->escapeIdentifiers($field); + return $sql.'DROP COLUMN '.$this->db->escapeIdentifiers($field); } - $sql .= ($alter_type === 'ADD') ? 'ADD ' : $alter_type . ' COLUMN '; + $sql .= ($alter_type === 'ADD') ? 'ADD ' : $alter_type.' COLUMN '; $sqls = []; - for ($i = 0, $c = count($field); $i < $c; $i ++ ) + for ($i = 0, $c = count($field); $i < $c; $i++) { $sqls[] = $sql - . ($field[$i]['_literal'] !== false ? $field[$i]['_literal'] : $this->_processColumn($field[$i])); + .($field[$i]['_literal'] !== false ? $field[$i]['_literal'] : $this->_processColumn($field[$i])); } return $sqls; @@ -844,16 +858,16 @@ protected function _processFields($create_table = false) isset($attributes['TYPE']) && $this->_attributeType($attributes); $field = [ - 'name' => $key, - 'new_name' => isset($attributes['NAME']) ? $attributes['NAME'] : null, - 'type' => isset($attributes['TYPE']) ? $attributes['TYPE'] : null, - 'length' => '', - 'unsigned' => '', - 'null' => '', - 'unique' => '', - 'default' => '', + 'name' => $key, + 'new_name' => isset($attributes['NAME']) ? $attributes['NAME'] : null, + 'type' => isset($attributes['TYPE']) ? $attributes['TYPE'] : null, + 'length' => '', + 'unsigned' => '', + 'null' => '', + 'unique' => '', + 'default' => '', 'auto_increment' => '', - '_literal' => false, + '_literal' => false, ]; isset($attributes['TYPE']) && $this->_attributeUnsigned($attributes, $field); @@ -866,7 +880,7 @@ protected function _processFields($create_table = false) } elseif (isset($attributes['FIRST'])) { - $field['first'] = (bool) $attributes['FIRST']; + $field['first'] = (bool)$attributes['FIRST']; } } @@ -876,7 +890,7 @@ protected function _processFields($create_table = false) { if ($attributes['NULL'] === true) { - $field['null'] = empty($this->null) ? '' : ' ' . $this->null; + $field['null'] = empty($this->null) ? '' : ' '.$this->null; } else { @@ -903,10 +917,12 @@ protected function _processFields($create_table = false) case 'ENUM': case 'SET': $attributes['CONSTRAINT'] = $this->db->escape($attributes['CONSTRAINT']); - $field['length'] = is_array($attributes['CONSTRAINT']) ? "('" . implode("','", $attributes['CONSTRAINT']) . "')" : '(' . $attributes['CONSTRAINT'] . ')'; + $field['length'] = is_array($attributes['CONSTRAINT']) ? "('".implode("','", + $attributes['CONSTRAINT'])."')" : '('.$attributes['CONSTRAINT'].')'; break; default: - $field['length'] = is_array($attributes['CONSTRAINT']) ? '(' . implode(',', $attributes['CONSTRAINT']) . ')' : '(' . $attributes['CONSTRAINT'] . ')'; + $field['length'] = is_array($attributes['CONSTRAINT']) ? '('.implode(',', + $attributes['CONSTRAINT']).')' : '('.$attributes['CONSTRAINT'].')'; break; } } @@ -929,12 +945,12 @@ protected function _processFields($create_table = false) protected function _processColumn($field) { return $this->db->escapeIdentifiers($field['name']) - . ' ' . $field['type'] . $field['length'] - . $field['unsigned'] - . $field['default'] - . $field['null'] - . $field['auto_increment'] - . $field['unique']; + .' '.$field['type'].$field['length'] + .$field['unsigned'] + .$field['default'] + .$field['null'] + .$field['auto_increment'] + .$field['unique']; } //-------------------------------------------------------------------- @@ -1027,15 +1043,15 @@ protected function _attributeDefault(&$attributes, &$field) { if ($attributes['DEFAULT'] === null) { - $field['default'] = empty($this->null) ? '' : $this->default . $this->null; + $field['default'] = empty($this->null) ? '' : $this->default.$this->null; // Override the NULL attribute if that's our default $attributes['NULL'] = true; - $field['null'] = empty($this->null) ? '' : ' ' . $this->null; + $field['null'] = empty($this->null) ? '' : ' '.$this->null; } else { - $field['default'] = $this->default . $this->db->escape($attributes['DEFAULT']); + $field['default'] = $this->default.$this->db->escape($attributes['DEFAULT']); } } } @@ -1052,7 +1068,7 @@ protected function _attributeDefault(&$attributes, &$field) */ protected function _attributeUnique(&$attributes, &$field) { - if ( ! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === true) + if (! empty($attributes['UNIQUE']) && $attributes['UNIQUE'] === true) { $field['unique'] = ' UNIQUE'; } @@ -1070,8 +1086,8 @@ protected function _attributeUnique(&$attributes, &$field) */ protected function _attributeAutoIncrement(&$attributes, &$field) { - if ( ! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true && - stripos($field['type'], 'int') !== false + if (! empty($attributes['AUTO_INCREMENT']) && $attributes['AUTO_INCREMENT'] === true + && stripos($field['type'], 'int') !== false ) { $field['auto_increment'] = ' AUTO_INCREMENT'; @@ -1091,9 +1107,9 @@ protected function _processPrimaryKeys($table) { $sql = ''; - for ($i = 0, $c = count($this->primaryKeys); $i < $c; $i ++ ) + for ($i = 0, $c = count($this->primaryKeys); $i < $c; $i++) { - if ( ! isset($this->fields[$this->primaryKeys[$i]])) + if (! isset($this->fields[$this->primaryKeys[$i]])) { unset($this->primaryKeys[$i]); } @@ -1101,8 +1117,8 @@ protected function _processPrimaryKeys($table) if (count($this->primaryKeys) > 0) { - $sql .= ",\n\tCONSTRAINT " . $this->db->escapeIdentifiers('pk_' . $table) - . ' PRIMARY KEY(' . implode(', ', $this->db->escapeIdentifiers($this->primaryKeys)) . ')'; + $sql .= ",\n\tCONSTRAINT ".$this->db->escapeIdentifiers('pk_'.$table) + .' PRIMARY KEY('.implode(', ', $this->db->escapeIdentifiers($this->primaryKeys)).')'; } return $sql; @@ -1121,13 +1137,13 @@ protected function _processIndexes($table) { $sqls = []; - for ($i = 0, $c = count($this->keys); $i < $c; $i ++ ) + for ($i = 0, $c = count($this->keys); $i < $c; $i++) { - $this->keys[$i] = (array) $this->keys[$i]; + $this->keys[$i] = (array)$this->keys[$i]; - for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2 ++ ) + for ($i2 = 0, $c2 = count($this->keys[$i]); $i2 < $c2; $i2++) { - if ( ! isset($this->fields[$this->keys[$i][$i2]])) + if (! isset($this->fields[$this->keys[$i][$i2]])) { unset($this->keys[$i][$i2]); } @@ -1137,47 +1153,53 @@ protected function _processIndexes($table) continue; } - $sqls[] = 'CREATE INDEX ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i])) - . ' ON ' . $this->db->escapeIdentifiers($table) - . ' (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');'; + $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; } //-------------------------------------------------------------------- - /** + + /** * Process foreign keys * * @param string $table Table name * * @return string */ - protected function _processForeignKeys($table) { - $sql = ''; - - $allowActions = array('CASCADE','SET NULL','NO ACTION','RESTRICT','SET DEFAULT'); - - if (count($this->foreignKeys) > 0){ - foreach ($this->foreignKeys as $field => $fkey) { - $name_index = $table.'_'.$field.'_foreign'; - - $sql .= ",\n\tCONSTRAINT " . $this->db->escapeIdentifiers($name_index) - . ' FOREIGN KEY(' . $this->db->escapeIdentifiers($field) . ') REFERENCES '.$this->db->escapeIdentifiers($this->db->DBPrefix.$fkey['table']).' ('.$this->db->escapeIdentifiers($fkey['field']).')'; - - if($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions)){ - $sql .= " ON DELETE ".$fkey['onDelete']; - } - - if($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions)){ - $sql .= " ON UPDATE ".$fkey['onDelete']; - } - - } - } - - return $sql; - } + protected function _processForeignKeys($table) + { + $sql = ''; + + $allowActions = ['CASCADE', 'SET NULL', 'NO ACTION', 'RESTRICT', 'SET DEFAULT']; + + if (count($this->foreignKeys) > 0) + { + foreach ($this->foreignKeys as $field => $fkey) + { + $name_index = $table.'_'.$field.'_foreign'; + + $sql .= ",\n\tCONSTRAINT ".$this->db->escapeIdentifiers($name_index) + .' FOREIGN KEY('.$this->db->escapeIdentifiers($field).') REFERENCES '.$this->db->escapeIdentifiers($this->db->DBPrefix.$fkey['table']).' ('.$this->db->escapeIdentifiers($fkey['field']).')'; + + if ($fkey['onDelete'] !== false && in_array($fkey['onDelete'], $allowActions)) + { + $sql .= " ON DELETE ".$fkey['onDelete']; + } + + if ($fkey['onUpdate'] !== false && in_array($fkey['onUpdate'], $allowActions)) + { + $sql .= " ON UPDATE ".$fkey['onDelete']; + } + + } + } + + return $sql; + } //-------------------------------------------------------------------- /** diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index ec2b0d24172f..bcc0ad62d2ae 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -27,200 +27,225 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * - * @package CodeIgniter - * @author CodeIgniter Dev Team - * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/) - * @license https://opensource.org/licenses/MIT MIT License - * @link https://codeigniter.com - * @since Version 3.0.0 + * @package CodeIgniter + * @author CodeIgniter Dev Team + * @copyright 2014-2017 British Columbia Institute of Technology (https://bcit.ca/) + * @license https://opensource.org/licenses/MIT MIT License + * @link https://codeigniter.com + * @since Version 3.0.0 * @filesource */ +use CodeIgniter\Database\Exceptions\DatabaseException; + /** * Forge for Postgre */ class Forge extends \CodeIgniter\Database\Forge { - /** - * UNSIGNED support - * - * @var bool|array - */ - protected $_unsigned = FALSE; - - /** - * NULL value representation in CREATE/ALTER TABLE statements - * - * @var string - */ - protected $_null = 'NULL'; - - //-------------------------------------------------------------------- - - /** - * Constructor. - * - */ - public function __construct($db) - { - parent::__construct($db); - - if (version_compare($this->db->getVersion(), '3.3', '<')) - { - $this->createTableIfStr = FALSE; - $this->dropTableIfStr = FALSE; - } - } - - //-------------------------------------------------------------------- - - /** - * Create database - * - * @param string $db_name - * - * @return bool - */ - public function createDatabase($db_name): 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 - * - * @param string $db_name - * - * @return bool - * @throws \CodeIgniter\DatabaseException - */ - public function dropDatabase($db_name): bool - { - // In SQLite, a database is dropped when we delete a file - if (!file_exists($db_name)) - { - 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($db_name)) - { - 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($db_name), array_map('strtolower', $this->db->dataCache['db_names']), true); - if ($key !== false) - { - unset($this->db->dataCache['db_names'][$key]); - } - } - - return true; - } - - //-------------------------------------------------------------------- - - /** - * ALTER TABLE - * - * @todo implement drop_column(), modify_column() - * @param string $alter_type ALTER type - * @param string $table Table name - * @param mixed $field Column definition - * - * @return string|array - */ - protected function _alterTable($alter_type, $table, $field) - { - if (in_array($alter_type, ['DROP', 'CHANGE'], true)) - { - return FALSE; - } - return parent::_alterTable($alter_type, $table, $field); - } - - //-------------------------------------------------------------------- - - /** - * Process column - * - * @param array $field - * @return string - */ - protected function _processColumn($field) - { - return $this->db->escapeIdentifiers($field['name']) - . ' ' . $field['type'] - . $field['auto_increment'] - . $field['null'] - . $field['unique'] - . $field['default']; - } - - //-------------------------------------------------------------------- - - /** - * Field attribute TYPE - * - * Performs a data type mapping between different databases. - * - * @param array &$attributes - * - * @return void - */ - protected function _attributeType(&$attributes) - { - switch (strtoupper($attributes['TYPE'])) { - case 'ENUM': - case 'SET': - $attributes['TYPE'] = 'TEXT'; - return; - default: - return; - } - } - - //-------------------------------------------------------------------- - - /** - * Field attribute AUTO_INCREMENT - * - * @param array &$attributes - * @param array &$field - * - * @return void - */ - protected function _attributeAutoIncrement(&$attributes, &$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 = array(); - } - } - - //-------------------------------------------------------------------- + /** + * UNSIGNED support + * + * @var bool|array + */ + protected $_unsigned = false; + + /** + * NULL value representation in CREATE/ALTER TABLE statements + * + * @var string + */ + protected $_null = 'NULL'; + + //-------------------------------------------------------------------- + + /** + * Constructor. + * + */ + public function __construct($db) + { + parent::__construct($db); + + if (version_compare($this->db->getVersion(), '3.3', '<')) + { + $this->createTableIfStr = false; + $this->dropTableIfStr = false; + } + } + + //-------------------------------------------------------------------- + + /** + * Create database + * + * @param string $db_name + * + * @return bool + */ + public function createDatabase($db_name): 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 + * + * @param string $db_name + * + * @return bool + * @throws \CodeIgniter\DatabaseException + */ + public function dropDatabase($db_name): bool + { + // In SQLite, a database is dropped when we delete a file + if (! file_exists($db_name)) + { + 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($db_name)) + { + 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($db_name), array_map('strtolower', $this->db->dataCache['db_names']), true); + if ($key !== false) + { + unset($this->db->dataCache['db_names'][$key]); + } + } + + return true; + } + + //-------------------------------------------------------------------- + + /** + * ALTER TABLE + * + * @todo implement drop_column(), modify_column() + * + * @param string $alter_type ALTER type + * @param string $table Table name + * @param mixed $field Column definition + * + * @return string|array + */ + protected function _alterTable($alter_type, $table, $field) + { + if (in_array($alter_type, ['DROP', 'CHANGE'], true)) + { + return false; + } + + return parent::_alterTable($alter_type, $table, $field); + } + + //-------------------------------------------------------------------- + + /** + * Process column + * + * @param array $field + * + * @return string + */ + protected function _processColumn($field) + { + return $this->db->escapeIdentifiers($field['name']) + .' '.$field['type'] + .$field['auto_increment'] + .$field['null'] + .$field['unique'] + .$field['default']; + } + + //-------------------------------------------------------------------- + + /** + * Field attribute TYPE + * + * Performs a data type mapping between different databases. + * + * @param array &$attributes + * + * @return void + */ + protected function _attributeType(&$attributes) + { + switch (strtoupper($attributes['TYPE'])) + { + case 'ENUM': + case 'SET': + $attributes['TYPE'] = 'TEXT'; + + return; + default: + return; + } + } + + //-------------------------------------------------------------------- + + /** + * Field attribute AUTO_INCREMENT + * + * @param array &$attributes + * @param array &$field + * + * @return void + */ + protected function _attributeAutoIncrement(&$attributes, &$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 + * + * @param string $table Table name + * @param string $foreign_name Foreign name + * + * @return bool + * @throws \CodeIgniter\Database\Exceptions\DatabaseException + */ + public function dropForeignKey($table, $foreign_name) + { + throw new DatabaseException(lang('Database.dropForeignKeyUnsupported')); + } + + //-------------------------------------------------------------------- + } diff --git a/system/Language/en/Database.php b/system/Language/en/Database.php index 197e6d90e1ce..51964f8ca8a2 100644 --- a/system/Language/en/Database.php +++ b/system/Language/en/Database.php @@ -2,4 +2,6 @@ return [ 'invalidEvent' => '{0, string} is not a valid Model Event callback.', + 'fieldNotExists' => 'Field "{0, string}" does not exist.', + 'dropForeignKeyUnsupported' => 'Dropping of foreign keys is not supported by the database driver.', ]; diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index f6210ece52ba..b0242a51cc3d 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -1,5 +1,7 @@ forge->dropTable('forge_test_table', true); $this->forge->addField([ - 'id' => [ + 'id' => [ 'type' => 'INTEGER', 'constraint' => 11, 'unsigned' => false, @@ -107,6 +109,13 @@ public function testAddFields() $this->assertEquals($fieldsData[1]->max_length, 255); } + elseif ($this->db->DBDriver === 'SQLite3') + { + $this->assertEquals(strtolower($fieldsData[0]->type), 'integer'); + $this->assertEquals(strtolower($fieldsData[1]->type), 'varchar'); + + $this->assertEquals($fieldsData[1]->default, null); + } else { $this->assertTrue(false, "DB Driver not supported"); @@ -118,14 +127,14 @@ public function testAddFields() public function testCompositeKey() { - // SQLite3 uses auto increment different - $unique_or_auto = $this->db->DBDriver == 'SQLite3' ? 'unique' : 'auto_increment'; + // SQLite3 uses auto increment different + $unique_or_auto = $this->db->DBDriver == 'SQLite3' ? 'unique' : 'auto_increment'; $this->forge->addField([ 'id' => [ - 'type' => 'INTEGER', - 'constraint' => 3, - $unique_or_auto => true, + 'type' => 'INTEGER', + 'constraint' => 3, + $unique_or_auto => true, ], 'code' => [ 'type' => 'VARCHAR', @@ -149,7 +158,6 @@ public function testCompositeKey() public function testForeignKey() { - $attributes = []; if ($this->db->DBDriver == 'MySQLi') @@ -191,7 +199,14 @@ public function testForeignKey() $foreignKeyData = $this->db->getForeignKeyData('forge_test_invoices'); - $this->assertEquals($foreignKeyData[0]->constraint_name,$this->db->DBPrefix.'forge_test_invoices_users_id_foreign'); + if ($this->db->DBDriver == 'SQLite3') + { + $this->assertEquals($foreignKeyData[0]->constraint_name, 'users_id to db_forge_test_users.id'); + } + else + { + $this->assertEquals($foreignKeyData[0]->constraint_name,$this->db->DBPrefix.'forge_test_invoices_users_id_foreign'); + } $this->assertEquals($foreignKeyData[0]->table_name, $this->db->DBPrefix.'forge_test_invoices'); $this->assertEquals($foreignKeyData[0]->foreign_table_name, $this->db->DBPrefix.'forge_test_users'); @@ -209,6 +224,10 @@ public function testDropForeignKey() { $attributes = ['ENGINE' => 'InnoDB']; } + if ($this->db->DBDriver == 'SQLite3') + { + $this->expectException(DatabaseException::class); + } $this->forge->addField([ 'id' => [ From e4ec33ba9cb782065612d1e3045cee98e48ca23e Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Wed, 25 Oct 2017 22:17:44 -0500 Subject: [PATCH 05/12] Small doc update --- user_guide_src/source/database/forge.rst | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/user_guide_src/source/database/forge.rst b/user_guide_src/source/database/forge.rst index cee68cb3b312..c063a20b7b00 100644 --- a/user_guide_src/source/database/forge.rst +++ b/user_guide_src/source/database/forge.rst @@ -183,23 +183,20 @@ below is for MySQL. Adding Foreign Keys =========== - -:: +Foreign Keys help to enforce relationships and actions across your tables. For tables that support Foreign Keys, +you may add them directly in forge:: $forge->addForeignKey('users_id','users','id'); // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) -You can specify the desired action for the "on delete" and "on update" properties of the constraint: - -:: +You can specify the desired action for the "on delete" and "on update" properties of the constraint:: $forge->addForeignKey('users_id','users','id','CASCADE','CASCADE'); // gives CONSTRAINT `TABLENAME_users_foreign` FOREIGN KEY(`users_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE - Creating a table ================ @@ -254,6 +251,7 @@ Execute a DROP FOREIGN KEY. // Produces: ALTER TABLE 'tablename' DROP FOREIGN KEY 'users_foreign' $forge->dropForeignKey('tablename','users_foreign'); +.. note:: SQlite database driver does not support dropping of foreign keys. Renaming a table ================ From 2875f86edab0c5290d891147ce7dcdc553f207ab Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Wed, 25 Oct 2017 22:31:42 -0500 Subject: [PATCH 06/12] Add Travis support for SQLite --- .travis.yml | 1 + tests/travis/Database.php | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index bcd0572155f7..412cde9bbcb8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,7 @@ dist: precise env: - DB=mysqli - DB=postgres + - DB=sqlite services: - memcached diff --git a/tests/travis/Database.php b/tests/travis/Database.php index 109cfdcb838a..397c608c93c1 100644 --- a/tests/travis/Database.php +++ b/tests/travis/Database.php @@ -42,6 +42,27 @@ 'compress' => false, 'strictOn' => false, 'failover' => [], - ] + ], + + 'sqlite' => [ + 'DSN' => '', + 'hostname' => 'localhost', + 'username' => '', + 'password' => '', + 'database' => ':memory:', + 'DBDriver' => 'SQLite3', + 'DBPrefix' => 'db_', + 'pConnect' => false, + 'DBDebug' => (ENVIRONMENT !== 'production'), + 'cacheOn' => false, + 'cacheDir' => '', + 'charset' => 'utf8', + 'DBCollat' => 'utf8_general_ci', + 'swapPre' => '', + 'encrypt' => false, + 'compress' => false, + 'strictOn' => false, + 'failover' => [], + ] ]; From 3eb509ae77cda4e1ef8e22c3e6de5afa9b6a65a0 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Tue, 30 Jan 2018 23:42:17 -0600 Subject: [PATCH 07/12] Trying to fix the is_unique tests --- tests/system/Validation/RulesTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index 8f6e8f1314d3..db27d681f342 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -2,8 +2,10 @@ use Config\Database; -class RulesTest extends \CIUnitTestCase +class RulesTest extends \CIDatabaseTestCase { + protected $refresh =true; + /** * @var Validation */ From 1c085b2035afcd70d3659ceca2a87d0974786cd3 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Tue, 30 Jan 2018 23:59:28 -0600 Subject: [PATCH 08/12] Validation test fixes --- php_errors.log | 1 + tests/system/Validation/RulesTest.php | 64 +----------- tests/system/Validation/UniqueRulesTest.php | 110 ++++++++++++++++++++ 3 files changed, 112 insertions(+), 63 deletions(-) create mode 100644 php_errors.log create mode 100644 tests/system/Validation/UniqueRulesTest.php diff --git a/php_errors.log b/php_errors.log new file mode 100644 index 000000000000..f246f51007b5 --- /dev/null +++ b/php_errors.log @@ -0,0 +1 @@ +[30-Jan-2018 23:57:36 America/Chicago] PHP Fatal error: Class 'z' not found in /Users/kilishan/WebSites/CodeIgniter4/tests/system/Validation/RulesTest.php on line 5 diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index db27d681f342..df737cffd293 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -2,10 +2,8 @@ use Config\Database; -class RulesTest extends \CIDatabaseTestCase +class RulesTest extends \CIUnitTestCase { - protected $refresh =true; - /** * @var Validation */ @@ -335,66 +333,6 @@ public function testDiffersFalse() //-------------------------------------------------------------------- - /** - * @group DatabaseLive - */ - public function testIsUniqueFalse() - { - $data = [ - 'email' => 'derek@world.com', - ]; - - $this->validation->setRules([ - 'email' => 'is_unique[user.email]', - ]); - - $this->assertFalse($this->validation->run($data)); - } - - //-------------------------------------------------------------------- - - /** - * @group DatabaseLive - */ - public function testIsUniqueTrue() - { - $data = [ - 'email' => 'derek@world.co.uk', - ]; - - $this->validation->setRules([ - 'email' => 'is_unique[user.email]', - ]); - - $this->assertTrue($this->validation->run($data)); - } - - //-------------------------------------------------------------------- - - /** - * @group DatabaseLive - */ - public function testIsUniqueIgnoresParams() - { - $db = Database::connect(); - $row = $db->table('user') - ->limit(1) - ->get() - ->getRow(); - - $data = [ - 'email' => 'derek@world.co.uk', - ]; - - $this->validation->setRules([ - 'email' => "is_unique[user.email,id,{$row->id}]", - ]); - - $this->assertTrue($this->validation->run($data)); - } - - //-------------------------------------------------------------------- - public function testMinLengthNull() { $data = [ diff --git a/tests/system/Validation/UniqueRulesTest.php b/tests/system/Validation/UniqueRulesTest.php new file mode 100644 index 000000000000..3e5a4ba73e22 --- /dev/null +++ b/tests/system/Validation/UniqueRulesTest.php @@ -0,0 +1,110 @@ + [ + \CodeIgniter\Validation\Rules::class, + \CodeIgniter\Validation\FormatRules::class, + \CodeIgniter\Validation\FileRules::class, + \CodeIgniter\Validation\CreditCardRules::class, + \CodeIgniter\Validation\TestRules::class, + ], + 'groupA' => [ + 'foo' => 'required|min_length[5]', + ], + 'groupA_errors' => [ + 'foo' => [ + 'min_length' => 'Shame, shame. Too short.', + ], + ], + ]; + + //-------------------------------------------------------------------- + + public function setUp() + { + parent::setUp(); + $this->validation = new Validation((object)$this->config, \Config\Services::renderer()); + $this->validation->reset(); + + $_FILES = []; + } + + /** + * @group DatabaseLive + */ + public function testIsUniqueFalse() + { + $this->hasInDatabase('user', [ + 'email' => 'derek@world.com' + ]); + + $data = [ + 'email' => 'derek@world.com', + ]; + + $this->validation->setRules([ + 'email' => 'is_unique[user.email]', + ]); + + $this->assertFalse($this->validation->run($data)); + } + + //-------------------------------------------------------------------- + + /** + * @group DatabaseLive + */ + public function testIsUniqueTrue() + { + $data = [ + 'email' => 'derek@world.co.uk', + ]; + + $this->validation->setRules([ + 'email' => 'is_unique[user.email]', + ]); + + $this->assertTrue($this->validation->run($data)); + } + + //-------------------------------------------------------------------- + + /** + * @group DatabaseLive + */ + public function testIsUniqueIgnoresParams() + { + $this->hasInDatabase('user', [ + 'email' => 'derek@world.co.uk' + ]); + + $db = Database::connect(); + $row = $db->table('user') + ->limit(1) + ->get() + ->getRow(); + + $data = [ + 'email' => 'derek@world.co.uk', + ]; + + $this->validation->setRules([ + 'email' => "is_unique[user.email,id,{$row->id}]", + ]); + + $this->assertTrue($this->validation->run($data)); + } + + //-------------------------------------------------------------------- +} From b7e06c2ffa73071871ba6d88afaa46814155a5e0 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 24 Aug 2018 23:38:11 -0500 Subject: [PATCH 09/12] Rules fixes --- tests/system/Validation/UniqueRulesTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/system/Validation/UniqueRulesTest.php b/tests/system/Validation/UniqueRulesTest.php index 3e5a4ba73e22..3b126fee85d7 100644 --- a/tests/system/Validation/UniqueRulesTest.php +++ b/tests/system/Validation/UniqueRulesTest.php @@ -1,8 +1,9 @@ [ 'foo' => 'required|min_length[5]', From 7173361170bf4944e1f3debe3e764318684f4c77 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 14 Sep 2018 23:19:45 -0500 Subject: [PATCH 10/12] Fix tests for Postgre --- tests/system/Validation/UniqueRulesTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/system/Validation/UniqueRulesTest.php b/tests/system/Validation/UniqueRulesTest.php index 3b126fee85d7..4d9d2d2ab60b 100644 --- a/tests/system/Validation/UniqueRulesTest.php +++ b/tests/system/Validation/UniqueRulesTest.php @@ -47,7 +47,9 @@ public function setUp() public function testIsUniqueFalse() { $this->hasInDatabase('user', [ - 'email' => 'derek@world.com' + 'name' => 'Derek', + 'email' => 'derek@world.com', + 'country' => 'USA', ]); $data = [ @@ -87,7 +89,9 @@ public function testIsUniqueTrue() public function testIsUniqueIgnoresParams() { $this->hasInDatabase('user', [ - 'email' => 'derek@world.co.uk' + 'name' => 'Derek', + 'email' => 'derek@world.co.uk', + 'country' => 'GB', ]); $db = Database::connect(); From ebd287b0568ffd7db104e338e1a4dbf18a289344 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 14 Sep 2018 23:45:11 -0500 Subject: [PATCH 11/12] Fix tests unique indexes for SQLite --- system/Database/SQLite3/Forge.php | 44 ++++++++++++++++++++++++ tests/system/Database/Live/ForgeTest.php | 8 ++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php index bcc0ad62d2ae..8de90fc74e3e 100644 --- a/system/Database/SQLite3/Forge.php +++ b/system/Database/SQLite3/Forge.php @@ -182,6 +182,50 @@ protected function _processColumn($field) //-------------------------------------------------------------------- + /** + * Process indexes + * + * @param string $table + * + * @return array + */ + protected function _processIndexes($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)) + { + $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 * diff --git a/tests/system/Database/Live/ForgeTest.php b/tests/system/Database/Live/ForgeTest.php index 4d1d647a491e..bf289e0e9009 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -1,5 +1,6 @@ db->DBDriver == 'SQLite3') + { + $this->markTestSkipped('SQLite3 does not support comments on tables or columns.'); + } + $this->forge->dropTable('forge_test_attributes', true); $this->forge->addField('id'); @@ -86,7 +92,7 @@ public function testAddFields() ]); $this->forge->addKey('id', true); - $this->forge->addKey(['username', 'active'], false, true); + $this->forge->addUniqueKey(['username', 'active']); $create = $this->forge->createTable('forge_test_fields', true); //Check Field names From 307bcd145efaa49db9786589ad0135c5a5f3dc08 Mon Sep 17 00:00:00 2001 From: Lonnie Ezell Date: Fri, 14 Sep 2018 23:50:27 -0500 Subject: [PATCH 12/12] [ci skip] Added SQLite3 to database requirements. --- user_guide_src/source/intro/requirements.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/user_guide_src/source/intro/requirements.rst b/user_guide_src/source/intro/requirements.rst index 5cf31ae50da2..68d3f12406c1 100644 --- a/user_guide_src/source/intro/requirements.rst +++ b/user_guide_src/source/intro/requirements.rst @@ -9,6 +9,7 @@ Currently supported databases are: - MySQL (5.1+) via the *MySQLi* driver - PostgreSQL via the *Postgre* driver + - SqLite3 via the *SQLite3* driver Not all of the drivers have been converted/rewritten for CodeIgniter4. The list below shows the outstanding ones. @@ -17,7 +18,7 @@ The list below shows the outstanding ones. - Oracle via the *oci8* and *pdo* drivers - PostgreSQL via the *pdo* driver - MS SQL via the *mssql*, *sqlsrv* (version 2005 and above only) and *pdo* drivers - - SQLite via the *sqlite* (version 2), *sqlite3* (version 3) and *pdo* drivers + - SQLite via the *sqlite* (version 2) and *pdo* drivers - CUBRID via the *cubrid* and *pdo* drivers - Interbase/Firebird via the *ibase* and *pdo* drivers - ODBC via the *odbc* and *pdo* drivers (you should know that ODBC is actually an abstraction layer)