diff --git a/.travis.yml b/.travis.yml index e043fbb41fbc..d07890f543ab 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ dist: precise env: - DB=mysqli - DB=postgres + - DB=sqlite services: - memcached 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/phpunit.xml.dist b/phpunit.xml.dist index 34d737b5a7b0..c3738f0a7c06 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,7 +16,10 @@ ./tests/system - + ./tests/system/Database + + + ./tests/system/Database diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index c4b0847b3656..77809da67554 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -188,6 +188,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; + //-------------------------------------------------------------------- /** @@ -1908,6 +1923,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); } @@ -2292,6 +2312,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/Forge.php b/system/Database/Forge.php index 2b94a1c5d907..e33ad79a0f6b 100644 --- a/system/Database/Forge.php +++ b/system/Database/Forge.php @@ -59,6 +59,7 @@ class Forge /** * List of keys. + * * @var array */ protected $keys = []; @@ -76,7 +77,7 @@ class Forge */ protected $primaryKeys = []; - /** + /** * List of foreign keys. * * @var type @@ -174,7 +175,7 @@ class Forge */ public function __construct(ConnectionInterface $db) { - $this->db = & $db; + $this->db = &$db; } //-------------------------------------------------------------------- @@ -210,7 +211,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) @@ -221,7 +222,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; } @@ -250,7 +251,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) { @@ -260,7 +261,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) @@ -287,7 +288,7 @@ public function addKey($key, bool $primary = false, bool $unique = false) { if ($primary === true) { - foreach ((array) $key as $one) + foreach ((array)$key as $one) { $this->primaryKeys[] = $one; } @@ -350,8 +351,8 @@ public function addField($field) { $this->addField([ 'id' => [ - 'type' => 'INT', - 'constraint' => 9, + 'type' => 'INT', + 'constraint' => 9, 'auto_increment' => true, ], ]); @@ -387,25 +388,25 @@ public function addField($field) * @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 DatabaseException(lang('Database.fieldNotExists', [$fieldName])); + } - if( ! isset($this->fields[$fieldName])) - { - throw new \RuntimeException('Field "'.$fieldName.'" not exist'); - } - - $this->foreignKeys[$fieldName] = [ - 'table' => $tableName, - 'field' => $tableField, - 'onDelete' => strtoupper($onDelete), - 'onUpdate' => strtoupper($onUpdate) - ]; + $this->foreignKeys[$fieldName] = [ + 'table' => $tableName, + 'field' => $tableField, + 'onDelete' => strtoupper($onDelete), + 'onUpdate' => strtoupper($onUpdate), + ]; - return $this; + return $this; } //-------------------------------------------------------------------- @@ -421,8 +422,8 @@ public function addForeignKey($fieldName= '',$tableName = '', $tableField = '', */ 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) { @@ -484,9 +485,9 @@ public function createTable($table, $if_not_exists = false, array $attributes = empty($this->db->dataCache['table_names']) || $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]); } @@ -522,18 +523,20 @@ protected function _createTable($table, $if_not_exists, $attributes) $if_not_exists = false; } - $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) @@ -542,7 +545,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; } @@ -578,7 +582,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 @@ -602,7 +606,7 @@ public function dropTable($table_name, $if_exists = false, $cascade = false) $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; } @@ -612,7 +616,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]); @@ -631,7 +636,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 */ @@ -643,7 +648,7 @@ protected function _dropTable($table, $if_exists, $cascade) { if ($this->dropTableIfStr === false) { - if ( ! $this->db->tableExists($table)) + if (! $this->db->tableExists($table)) { return true; } @@ -654,7 +659,7 @@ protected function _dropTable($table, $if_exists, $cascade) } } - $sql = $sql . ' ' . $this->db->escapeIdentifiers($table); + $sql = $sql.' '.$this->db->escapeIdentifiers($table); return $sql; } @@ -686,15 +691,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; } } @@ -706,11 +714,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) { @@ -722,7 +730,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) { @@ -734,7 +742,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) { @@ -758,7 +766,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) @@ -798,7 +806,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) { @@ -810,7 +818,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) { @@ -834,21 +842,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; @@ -885,16 +893,16 @@ protected function _processFields($create_table = false) isset($attributes['TYPE']) && $this->_attributeType($attributes); $field = [ - 'name' => $key, - 'new_name' => $attributes['NAME'] ?? null, - '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); @@ -907,7 +915,7 @@ protected function _processFields($create_table = false) } elseif (isset($attributes['FIRST'])) { - $field['first'] = (bool) $attributes['FIRST']; + $field['first'] = (bool)$attributes['FIRST']; } } @@ -917,7 +925,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 { @@ -944,10 +952,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; } } @@ -970,12 +980,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']; } //-------------------------------------------------------------------- @@ -1068,15 +1078,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']); } } } @@ -1093,7 +1103,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'; } @@ -1111,8 +1121,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'; @@ -1132,9 +1142,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]); } @@ -1142,8 +1152,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; @@ -1162,13 +1172,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]); } @@ -1180,22 +1190,23 @@ protected function _processIndexes($table) if (in_array($i, $this->uniqueKeys)) { - $sqls[] = 'ALTER TABLE ' . $this->db->escapeIdentifiers($table) - . ' ADD CONSTRAINT ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i])) - . ' UNIQUE (' . implode(', ', $this->db->escapeIdentifiers($this->keys[$i])) . ');'; + $sqls[] = 'ALTER TABLE '.$this->db->escapeIdentifiers($table) + .' ADD CONSTRAINT '.$this->db->escapeIdentifiers($table.'_'.implode('_', $this->keys[$i])) + .' UNIQUE ('.implode(', ', $this->db->escapeIdentifiers($this->keys[$i])).');'; continue; } - $sqls[] = 'CREATE INDEX ' . $this->db->escapeIdentifiers($table . '_' . implode('_', $this->keys[$i])) - . ' ON ' . $this->db->escapeIdentifiers($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 @@ -1203,30 +1214,31 @@ protected function _processIndexes($table) * @return string */ protected function _processForeignKeys($table) { - $sql = ''; + $sql = ''; - $allowActions = array('CASCADE','SET NULL','NO ACTION','RESTRICT','SET DEFAULT'); + $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'; + 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']).')'; + $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['onUpdate']; - } + 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['onUpdate']; } - } - return $sql; + } } + + return $sql; + } + //-------------------------------------------------------------------- /** diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php new file mode 100644 index 000000000000..1c6f4c2eed07 --- /dev/null +++ b/system/Database/SQLite3/Builder.php @@ -0,0 +1,107 @@ +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] = []; + + 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 = []; + 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 ['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); + } + + //-------------------------------------------------------------------- + + /** + * 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/system/Database/SQLite3/Forge.php b/system/Database/SQLite3/Forge.php new file mode 100644 index 000000000000..8de90fc74e3e --- /dev/null +++ b/system/Database/SQLite3/Forge.php @@ -0,0 +1,295 @@ +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']; + } + + //-------------------------------------------------------------------- + + /** + * 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 + * + * 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/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 @@ +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 55e7112ad00b..bf289e0e9009 100644 --- a/tests/system/Database/Live/ForgeTest.php +++ b/tests/system/Database/Live/ForgeTest.php @@ -1,5 +1,6 @@ forge->dropTable('forge_test_table', true); $this->forge->addField([ - 'id' => [ + 'id' => [ 'type' => 'INTEGER', 'constraint' => 11, 'unsigned' => false, @@ -41,6 +42,11 @@ public function testCreateTable() public function testCreateTableWithAttributes() { + if ($this->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 @@ -96,7 +102,6 @@ public function testAddFields() $this->assertContains('name', $fieldsNames); $this->assertContains('active', $fieldsNames); - $fieldsData = $this->db->getFieldData('forge_test_fields'); $this->assertContains($fieldsData[0]->name, ['id', 'name', 'username', 'active']); @@ -129,6 +134,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"); @@ -140,11 +152,14 @@ public function testAddFields() 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, + 'type' => 'INTEGER', + 'constraint' => 3, + $unique_or_auto => true, ], 'code' => [ 'type' => 'VARCHAR', @@ -196,7 +211,6 @@ public function testCompositeKey() public function testForeignKey() { - $attributes = []; if ($this->db->DBDriver == 'MySQLi') @@ -238,8 +252,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'); @@ -257,6 +277,10 @@ public function testDropForeignKey() { $attributes = ['ENGINE' => 'InnoDB']; } + if ($this->db->DBDriver == 'SQLite3') + { + $this->expectException(DatabaseException::class); + } $this->forge->addField([ 'id' => [ diff --git a/tests/system/Validation/RulesTest.php b/tests/system/Validation/RulesTest.php index 62f871cee216..71fa51180d9e 100644 --- a/tests/system/Validation/RulesTest.php +++ b/tests/system/Validation/RulesTest.php @@ -6,7 +6,6 @@ class RulesTest extends \CIUnitTestCase { - /** * @var Validation */ diff --git a/tests/system/Validation/UniqueRulesTest.php b/tests/system/Validation/UniqueRulesTest.php new file mode 100644 index 000000000000..4d9d2d2ab60b --- /dev/null +++ b/tests/system/Validation/UniqueRulesTest.php @@ -0,0 +1,115 @@ + [ + \CodeIgniter\Validation\Rules::class, + \CodeIgniter\Validation\FormatRules::class, + \CodeIgniter\Validation\FileRules::class, + \CodeIgniter\Validation\CreditCardRules::class, + \Tests\Support\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', [ + 'name' => 'Derek', + 'email' => 'derek@world.com', + 'country' => 'USA', + ]); + + $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', [ + 'name' => 'Derek', + 'email' => 'derek@world.co.uk', + 'country' => 'GB', + ]); + + $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)); + } + + //-------------------------------------------------------------------- +} 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' => [], + ] ]; diff --git a/user_guide_src/source/database/forge.rst b/user_guide_src/source/database/forge.rst index 35e465834e12..e72f462d9290 100644 --- a/user_guide_src/source/database/forge.rst +++ b/user_guide_src/source/database/forge.rst @@ -183,6 +183,9 @@ and unique keys with specific methods:: $forge->addPrimaryKey('blog_id'); // gives PRIMARY KEY `blog_id` (`blog_id`) +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->addUniqueKey(array('blog_id', 'uri')); // gives UNIQUE KEY `blog_id_uri` (`blog_id`, `uri`) @@ -195,9 +198,7 @@ Adding Foreign Keys $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 @@ -254,6 +255,8 @@ 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 ================ 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)