diff --git a/system/Common.php b/system/Common.php index 213c6dd808a0..c00e01bee534 100644 --- a/system/Common.php +++ b/system/Common.php @@ -815,7 +815,7 @@ function force_https(int $duration = 31536000, RequestInterface $request = null, $response->setHeader('Strict-Transport-Security', 'max-age=' . $duration); $response->redirect($uri); $response->sendHeaders(); - + exit(); } } diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index 0306651b2e85..942326104c9e 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -166,6 +166,15 @@ class BaseBuilder */ protected $QBWhereGroupCount = 0; + /** + * Ignore data that cause certain + * exceptions, for example in case of + * duplicate keys. + * + * @var boolean + */ + protected $QBIgnore = false; + /** * A reference to the database connection. * @@ -218,6 +227,14 @@ class BaseBuilder */ protected $canLimitWhereUpdates = true; + /** + * Specifies which sql statements + * support the ignore option. + * + * @var array + */ + protected $supportedIgnoreStatements = []; + //-------------------------------------------------------------------- /** @@ -266,6 +283,25 @@ public function getBinds(): array //-------------------------------------------------------------------- + /** + * Ignore + * + * Set ignore Flag for next insert, + * update or delete query. + * + * @param boolean $ignore + * + * @return BaseBuilder + */ + public function ignore(bool $ignore = true) + { + $this->QBIgnore = $ignore; + + return $this; + } + + //-------------------------------------------------------------------- + /** * Select * @@ -1705,7 +1741,7 @@ public function insertBatch(array $set = null, bool $escape = null, int $batchSi */ protected function _insertBatch(string $table, array $keys, array $values): string { - return 'INSERT INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES ' . implode(', ', $values); + return 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES ' . implode(', ', $values); } //-------------------------------------------------------------------- @@ -1774,6 +1810,8 @@ public function setInsertBatch($key, string $value = '', bool $escape = null) * * @param boolean $reset TRUE: reset QB values; FALSE: leave QB values alone * + * @throws DatabaseException + * * @return string */ public function getCompiledInsert(bool $reset = true): string @@ -1808,6 +1846,8 @@ public function getCompiledInsert(bool $reset = true): string * @param boolean $escape Whether to escape values and identifiers * @param boolean $test Used when running tests * + * @throws DatabaseException + * * @return BaseResult|Query|false */ public function insert(array $set = null, bool $escape = null, bool $test = false) @@ -1885,7 +1925,7 @@ protected function validateInsert(): bool */ protected function _insert(string $table, array $keys, array $unescapedKeys): string { - return 'INSERT INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $unescapedKeys) . ')'; + return 'INSERT ' . $this->compileIgnore('insert') . 'INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $unescapedKeys) . ')'; } //-------------------------------------------------------------------- @@ -2001,6 +2041,8 @@ public function getCompiledUpdate(bool $reset = true): string * @param integer $limit * @param boolean $test Are we testing the code? * + * @throws DatabaseException + * * @return boolean TRUE on success, FALSE on failure */ public function update(array $set = null, $where = null, int $limit = null, bool $test = false): bool @@ -2064,14 +2106,14 @@ public function update(array $set = null, $where = null, int $limit = null, bool */ protected function _update(string $table, array $values): string { - $valstr = []; + $valStr = []; foreach ($values as $key => $val) { - $valstr[] = $key . ' = ' . $val; + $valStr[] = $key . ' = ' . $val; } - return 'UPDATE ' . $table . ' SET ' . implode(', ', $valstr) + return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . implode(', ', $valStr) . $this->compileWhereHaving('QBWhere') . $this->compileOrderBy() . ($this->QBLimit ? $this->_limit(' ') : ''); @@ -2201,6 +2243,7 @@ protected function _updateBatch(string $table, array $values, string $index): st { $ids = []; $final = []; + foreach ($values as $key => $val) { $ids[] = $val[$index]; @@ -2224,7 +2267,7 @@ protected function _updateBatch(string $table, array $values, string $index): st $this->where($index . ' IN(' . implode(',', $ids) . ')', null, false); - return 'UPDATE ' . $table . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere'); + return 'UPDATE ' . $this->compileIgnore('update') . $table . ' SET ' . substr($cases, 0, -2) . $this->compileWhereHaving('QBWhere'); } //-------------------------------------------------------------------- @@ -2481,7 +2524,7 @@ public function decrement(string $column, int $value = 1) */ protected function _delete(string $table): string { - return 'DELETE FROM ' . $table . $this->compileWhereHaving('QBWhere') + return 'DELETE ' . $this->compileIgnore('delete') . 'FROM ' . $table . $this->compileWhereHaving('QBWhere') . ($this->QBLimit ? ' LIMIT ' . $this->QBLimit : ''); } @@ -2597,6 +2640,32 @@ protected function compileSelect($select_override = false): string //-------------------------------------------------------------------- + /** + * Compile Ignore Statement + * + * Checks if the ignore option is supported by + * the Database Driver for the specific statement. + * + * @param string $statement + * + * @return string + */ + protected function compileIgnore(string $statement) + { + $sql = ''; + + if ($this->QBIgnore && + isset($this->supportedIgnoreStatements[$statement]) + ) + { + $sql = trim($this->supportedIgnoreStatements[$statement]) . ' '; + } + + return $sql; + } + + //-------------------------------------------------------------------- + /** * Compile WHERE, HAVING statements * @@ -2918,6 +2987,7 @@ protected function resetWrite() 'QBOrderBy' => [], 'QBKeys' => [], 'QBLimit' => false, + 'QBIgnore' => false, ]); } diff --git a/system/Database/MySQLi/Builder.php b/system/Database/MySQLi/Builder.php index 6bfe554d732b..265fb6d14857 100644 --- a/system/Database/MySQLi/Builder.php +++ b/system/Database/MySQLi/Builder.php @@ -53,4 +53,16 @@ class Builder extends BaseBuilder */ protected $escapeChar = '`'; + /** + * Specifies which sql statements + * support the ignore option. + * + * @var array + */ + protected $supportedIgnoreStatements = [ + 'update' => 'IGNORE', + 'insert' => 'IGNORE', + 'delete' => 'IGNORE', + ]; + } diff --git a/system/Database/Postgre/Builder.php b/system/Database/Postgre/Builder.php index 9704c8c47736..92915ac77ecb 100644 --- a/system/Database/Postgre/Builder.php +++ b/system/Database/Postgre/Builder.php @@ -39,6 +39,7 @@ use CodeIgniter\Database\BaseBuilder; use CodeIgniter\Database\Exceptions\DatabaseException; +use http\Encoding\Stream\Inflate; /** * Builder for Postgre @@ -55,6 +56,40 @@ class Builder extends BaseBuilder 'RANDOM()', ]; + /** + * Specifies which sql statements + * support the ignore option. + * + * @var array + */ + protected $supportedIgnoreStatements = [ + 'insert' => 'ON CONFLICT DO NOTHING', + ]; + + //-------------------------------------------------------------------- + + /** + * Compile Ignore Statement + * + * Checks if the ignore option is supported by + * the Database Driver for the specific statement. + * + * @param string $statement + * + * @return string + */ + protected function compileIgnore(string $statement) + { + $sql = parent::compileIgnore($statement); + + if (! empty($sql)) + { + $sql = ' ' . trim($sql); + } + + return $sql; + } + //-------------------------------------------------------------------- /** @@ -97,6 +132,8 @@ public function orderBy(string $orderBy, string $direction = '', bool $escape = * @param string $column * @param integer $value * + * @throws DatabaseException + * * @return mixed */ public function increment(string $column, int $value = 1) @@ -116,6 +153,8 @@ public function increment(string $column, int $value = 1) * @param string $column * @param integer $value * + * @throws DatabaseException + * * @return mixed */ public function decrement(string $column, int $value = 1) @@ -129,6 +168,42 @@ public function decrement(string $column, int $value = 1) //-------------------------------------------------------------------- + /** + * Insert batch statement + * + * Generates a platform-specific insert string from the supplied data. + * + * @param string $table Table name + * @param array $keys INSERT keys + * @param array $values INSERT values + * + * @return string + */ + protected function _insertBatch(string $table, array $keys, array $values) : string + { + return 'INSERT INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES ' . implode(', ', $values) . $this->compileIgnore('insert'); + } + + //-------------------------------------------------------------------- + + /** + * Insert statement + * + * Generates a platform-specific insert string from the supplied data + * + * @param string $table The table name + * @param array $keys The insert keys + * @param array $unescapedKeys The insert values + * + * @return string + */ + protected function _insert(string $table, array $keys, array $unescapedKeys) : string + { + return 'INSERT INTO ' . $table . ' (' . implode(', ', $keys) . ') VALUES (' . implode(', ', $unescapedKeys) . ')' . $this->compileIgnore('insert'); + } + + //-------------------------------------------------------------------- + /** * Replace * diff --git a/system/Database/SQLite3/Builder.php b/system/Database/SQLite3/Builder.php index ba09b58d2eac..4dd67ef9eeeb 100644 --- a/system/Database/SQLite3/Builder.php +++ b/system/Database/SQLite3/Builder.php @@ -69,6 +69,13 @@ class Builder extends BaseBuilder */ protected $canLimitWhereUpdates = false; + /** + * @var array + */ + protected $supportedIgnoreStatements = [ + 'insert' => 'OR IGNORE', + ]; + //-------------------------------------------------------------------- /** @@ -105,6 +112,4 @@ protected function _truncate(string $table): string return 'DELETE FROM ' . $table; } - //-------------------------------------------------------------------- - } diff --git a/system/Debug/Toolbar/Views/toolbarloader.js.php b/system/Debug/Toolbar/Views/toolbarloader.js.php index 539490bc57f0..236ea014b7f5 100644 --- a/system/Debug/Toolbar/Views/toolbarloader.js.php +++ b/system/Debug/Toolbar/Views/toolbarloader.js.php @@ -62,9 +62,9 @@ function loadDoc(time) { // Track all AJAX requests if (window.ActiveXObject) { - var oldXHR = new ActiveXObject('Microsoft.XMLHTTP'); + var oldXHR = new ActiveXObject('Microsoft.XMLHTTP'); } else { - var oldXHR = window.XMLHttpRequest; + var oldXHR = window.XMLHttpRequest; } function newXHR() { diff --git a/system/Language/en/Entity.php b/system/Language/en/Entity.php index 36d5f4725aa3..71f9e76481d1 100644 --- a/system/Language/en/Entity.php +++ b/system/Language/en/Entity.php @@ -15,5 +15,5 @@ return [ - 'tryingToAccessNonExistentProperty' => 'Trying to access non existent property {0} of {1}' + 'tryingToAccessNonExistentProperty' => 'Trying to access non existent property {0} of {1}', ]; diff --git a/system/Router/Exceptions/RedirectException.php b/system/Router/Exceptions/RedirectException.php index 151e7302eaf0..c1f68620b46e 100644 --- a/system/Router/Exceptions/RedirectException.php +++ b/system/Router/Exceptions/RedirectException.php @@ -5,10 +5,11 @@ /** * Redirect exception */ + class RedirectException extends \Exception { public static function forUnableToRedirect(string $route, string $code) { return new static(lang('Redirect.forUnableToRedirect', [$route, $code])); } -} \ No newline at end of file +} diff --git a/tests/_support/Database/MockConnection.php b/tests/_support/Database/MockConnection.php index 699595bbfc3a..3b6fd2a94e14 100644 --- a/tests/_support/Database/MockConnection.php +++ b/tests/_support/Database/MockConnection.php @@ -141,7 +141,7 @@ public function getVersion(): string /** * Executes the query against the database. * - * @param string $sql + * @param string $sql * * @return mixed */ @@ -155,7 +155,7 @@ protected function execute(string $sql) /** * Returns the total number of rows affected by this query. * - * @return int + * @return integer */ public function affectedRows(): int { diff --git a/tests/_support/Database/MockTestClass.php b/tests/_support/Database/MockTestClass.php index 232943b10692..18e982a23241 100644 --- a/tests/_support/Database/MockTestClass.php +++ b/tests/_support/Database/MockTestClass.php @@ -4,4 +4,4 @@ class MockTestClass { -} \ No newline at end of file +} diff --git a/tests/system/Database/Builder/DeleteTest.php b/tests/system/Database/Builder/DeleteTest.php index 2c616693735b..88c3207b170d 100644 --- a/tests/system/Database/Builder/DeleteTest.php +++ b/tests/system/Database/Builder/DeleteTest.php @@ -35,5 +35,4 @@ public function testDelete() $this->assertEquals($expectedBinds, $builder->getBinds()); } - //-------------------------------------------------------------------- } diff --git a/tests/system/Database/Live/DbUtilsTest.php b/tests/system/Database/Live/DbUtilsTest.php index f355d94084d3..6fc89a11eec0 100644 --- a/tests/system/Database/Live/DbUtilsTest.php +++ b/tests/system/Database/Live/DbUtilsTest.php @@ -123,7 +123,7 @@ public function testUtilsOptimizeTableFalseOptimizeDatabase() { $util = (new Database())->loadUtils($this->db); - $this->setPrivateProperty($util,'optimizeTable', false); + $this->setPrivateProperty($util, 'optimizeTable', false); $this->expectException(DatabaseException::class); $this->expectExceptionMessage('Unsupported feature of the database platform you are using.'); @@ -155,7 +155,7 @@ public function testUtilsOptimizeTableFalseOptimizeTable() { $util = (new Database())->loadUtils($this->db); - $this->setPrivateProperty($util,'optimizeTable', false); + $this->setPrivateProperty($util, 'optimizeTable', false); $this->expectException(DatabaseException::class); $this->expectExceptionMessage('Unsupported feature of the database platform you are using.'); @@ -180,7 +180,7 @@ public function testUtilsRepairTable() public function testUtilsCSVFromResult() { $data = $this->db->table('job') - ->get(); + ->get(); $util = (new Database())->loadUtils($this->db); @@ -196,8 +196,8 @@ public function testUtilsCSVFromResult() public function testUtilsXMLFromResult() { $data = $this->db->table('job') - ->where('id', 4) - ->get(); + ->where('id', 4) + ->get(); $util = (new Database())->loadUtils($this->db); diff --git a/tests/system/Database/Live/GetTest.php b/tests/system/Database/Live/GetTest.php index 002d76861986..35ddc5e5dc65 100644 --- a/tests/system/Database/Live/GetTest.php +++ b/tests/system/Database/Live/GetTest.php @@ -15,8 +15,8 @@ class GetTest extends CIDatabaseTestCase public function testGet() { $jobs = $this->db->table('job') - ->get() - ->getResult(); + ->get() + ->getResult(); $this->assertCount(4, $jobs); $this->assertEquals('Developer', $jobs[0]->name); @@ -30,8 +30,8 @@ public function testGet() public function testGetWitLimit() { $jobs = $this->db->table('job') - ->get(2, 2) - ->getResult(); + ->get(2, 2) + ->getResult(); $this->assertCount(2, $jobs); $this->assertEquals('Accountant', $jobs[0]->name); @@ -43,8 +43,8 @@ public function testGetWitLimit() public function testGetWhereArray() { $jobs = $this->db->table('job') - ->getWhere(['id' => 1]) - ->getResult(); + ->getWhere(['id' => 1]) + ->getResult(); $this->assertCount(1, $jobs); $this->assertEquals('Developer', $jobs[0]->name); @@ -55,8 +55,8 @@ public function testGetWhereArray() public function testGetWhereWithLimits() { $jobs = $this->db->table('job') - ->getWhere('id > 1', 1, 1) - ->getResult(); + ->getWhere('id > 1', 1, 1) + ->getResult(); $this->assertCount(1, $jobs); $this->assertEquals('Accountant', $jobs[0]->name); @@ -67,8 +67,8 @@ public function testGetWhereWithLimits() public function testGetFieldCount() { $jobs = $this->db->table('job') - ->get() - ->getFieldCount(); + ->get() + ->getFieldCount(); $this->assertEquals(6, $jobs); } @@ -78,8 +78,8 @@ public function testGetFieldCount() public function testGetFieldNames() { $jobs = $this->db->table('job') - ->get() - ->getFieldNames(); + ->get() + ->getFieldNames(); $this->assertTrue(in_array('name', $jobs)); $this->assertTrue(in_array('description', $jobs)); @@ -90,8 +90,8 @@ public function testGetFieldNames() public function testGetFieldData() { $jobs = $this->db->table('job') - ->get() - ->getFieldData(); + ->get() + ->getFieldData(); $this->assertEquals('id', $jobs[0]->name); $this->assertEquals('name', $jobs[1]->name); @@ -102,7 +102,7 @@ public function testGetFieldData() public function testGetDataSeek() { $data = $this->db->table('job') - ->get(); + ->get(); if ($this->db->DBDriver === 'SQLite3') { @@ -120,7 +120,7 @@ public function testGetDataSeek() public function testGetAnotherDataSeek() { $data = $this->db->table('job') - ->get(); + ->get(); $data->dataSeek(0); @@ -137,8 +137,8 @@ public function testGetAnotherDataSeek() public function testFreeResult() { $data = $this->db->table('job') - ->where('id', 4) - ->get(); + ->where('id', 4) + ->get(); $details = $data->getResult(); @@ -154,8 +154,8 @@ public function testFreeResult() public function testGetRowWithColumnName() { $name = $this->db->table('user') - ->get() - ->getRow('name', 'array'); + ->get() + ->getRow('name', 'array'); $this->assertEquals('Derek Jones', $name); } @@ -165,8 +165,8 @@ public function testGetRowWithColumnName() public function testGetRowWithReturnType() { $user = $this->db->table('user') - ->get() - ->getRow(0, 'array'); + ->get() + ->getRow(0, 'array'); $this->assertEquals('Derek Jones', $user['name']); } @@ -175,11 +175,9 @@ public function testGetRowWithReturnType() public function testGetRowWithCustomReturnType() { - $user = $this->db->table('user') - ->get() - ->getRow(0, 'Tests\Support\Database\MockTestClass'); - + ->get() + ->getRow(0, 'Tests\Support\Database\MockTestClass'); $this->assertEquals('Derek Jones', $user->name); } @@ -188,10 +186,9 @@ public function testGetRowWithCustomReturnType() public function testGetFirstRow() { - $user = $this->db->table('user') - ->get() - ->getFirstRow(); + ->get() + ->getFirstRow(); $this->assertEquals('Derek Jones', $user->name); } @@ -200,10 +197,9 @@ public function testGetFirstRow() public function testGetLastRow() { - $user = $this->db->table('user') - ->get() - ->getLastRow(); + ->get() + ->getLastRow(); $this->assertEquals('Chris Martin', $user->name); } @@ -212,10 +208,9 @@ public function testGetLastRow() public function testGetNextRow() { - $user = $this->db->table('user') - ->get() - ->getNextRow(); + ->get() + ->getNextRow(); $this->assertEquals('Ahmadinejad', $user->name); } @@ -225,13 +220,13 @@ public function testGetNextRow() public function testGetPreviousRow() { $user = $this->db->table('user') - ->get(); + ->get(); $user->currentRow = 3; - $user = $user->getPreviousRow(); + $user = $user->getPreviousRow(); $this->assertEquals('Richard A Causey', $user->name); } //-------------------------------------------------------------------- -} \ No newline at end of file +} diff --git a/tests/system/Database/Live/WhereTest.php b/tests/system/Database/Live/WhereTest.php index 82208d924e6d..0c9685c144b7 100644 --- a/tests/system/Database/Live/WhereTest.php +++ b/tests/system/Database/Live/WhereTest.php @@ -124,14 +124,14 @@ public function testWhereNotIn() public function testSubQuery() { $subQuery = $this->db->table('job') - ->select('id') - ->where('name', 'Developer') - ->getCompiledSelect(); - + ->select('id') + ->where('name', 'Developer') + ->getCompiledSelect(); + $jobs = $this->db->table('job') - ->where('id not in (' . $subQuery . ')', null, false) - ->get() - ->getResult(); + ->where('id not in (' . $subQuery . ')', null, false) + ->get() + ->getResult(); $this->assertCount(3, $jobs); $this->assertEquals('Politician', $jobs[0]->name); @@ -144,14 +144,14 @@ public function testSubQuery() public function testSubQueryAnotherType() { $subQuery = $this->db->table('job') - ->select('id') - ->where('name', 'Developer') - ->getCompiledSelect(); + ->select('id') + ->where('name', 'Developer') + ->getCompiledSelect(); $jobs = $this->db->table('job') - ->where('id = (' . $subQuery . ')', null, false) - ->get() - ->getResult(); + ->where('id = (' . $subQuery . ')', null, false) + ->get() + ->getResult(); $this->assertCount(1, $jobs); $this->assertEquals('Developer', $jobs[0]->name); diff --git a/tests/system/Database/Migrations/MigrationTest.php b/tests/system/Database/Migrations/MigrationTest.php index b4d51bfc0f65..a41208961952 100644 --- a/tests/system/Database/Migrations/MigrationTest.php +++ b/tests/system/Database/Migrations/MigrationTest.php @@ -13,8 +13,12 @@ public function testDBGroup() { $migration = new class extends Migration { protected $DBGroup = 'tests'; - function up(){} - function down(){} + function up() + { + } + function down() + { + } }; $dbGroup = $migration->getDBGroup(); diff --git a/tests/system/EntityTest.php b/tests/system/EntityTest.php index 61cb74068133..b6a58e890172 100644 --- a/tests/system/EntityTest.php +++ b/tests/system/EntityTest.php @@ -667,7 +667,7 @@ public function testHasChangedNoChange() public function testHasChangedWholeEntity() { $entity = $this->getEntity(); - + $entity->foo = 'bar'; $this->assertTrue($entity->hasChanged()); diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index 6bf1efcc6739..fa8e32b3ccfa 100755 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -622,6 +622,22 @@ The first parameter is an object. .. note:: All values are escaped automatically producing safer queries. +**$builder->ignore()** + +Generates an insert ignore string based on the data you supply, and runs the +query. So if an entry with the same primary key already exists, the query won't be inserted. +You can optionally pass an **boolean** to the function. Here is an example using the array of the above example:: + + $data = [ + 'title' => 'My title', + 'name' => 'My Name', + 'date' => 'My date' + ]; + + $builder->ignore(true)->insert($data); + // Produces: INSERT OR IGNORE INTO mytable (title, name, date) VALUES ('My title', 'My name', 'My date') + + **$builder->getCompiledInsert()** Compiles the insertion query just like $builder->insert() but does not