diff --git a/system/BaseModel.php b/system/BaseModel.php index c4006afc0a51..04c3a3f63e31 100644 --- a/system/BaseModel.php +++ b/system/BaseModel.php @@ -357,7 +357,7 @@ abstract protected function doFirst(); * * @param array $data Data * - * @return object|integer|string|false + * @return integer|string|boolean */ abstract protected function doInsert(array $data); @@ -407,7 +407,7 @@ abstract protected function doUpdateBatch(array $set = null, string $index = nul * @param integer|string|array|null $id The rows primary key(s) * @param boolean $purge Allows overriding the soft deletes setting. * - * @return object|boolean + * @return string|boolean * * @throws DatabaseException */ @@ -666,11 +666,7 @@ public function save($data): bool { $response = $this->insert($data, false); - if ($response instanceof BaseResult) - { - $response = $response->resultID !== false; - } - elseif ($response !== false) + if ($response !== false) { $response = true; } @@ -708,7 +704,7 @@ public function getInsertID() * @param array|object|null $data Data * @param boolean $returnID Whether insert ID should be returned or not. * - * @return BaseResult|object|integer|string|false + * @return integer|string|boolean * * @throws ReflectionException */ diff --git a/system/Database/BaseBuilder.php b/system/Database/BaseBuilder.php index c08b05469836..30f6e2100078 100644 --- a/system/Database/BaseBuilder.php +++ b/system/Database/BaseBuilder.php @@ -2252,7 +2252,7 @@ public function getCompiledInsert(bool $reset = true) * * @throws DatabaseException * - * @return BaseResult|Query|false + * @return Query|boolean */ public function insert(array $set = null, bool $escape = null) { @@ -2483,7 +2483,7 @@ public function update(array $set = null, $where = null, int $limit = null): boo $result = $this->db->query($sql, $this->binds, false); - if ($result->resultID !== false) + if ($result !== false) { // Clear our binds so we don't eat up memory $this->binds = []; @@ -2835,7 +2835,7 @@ public function getCompiledDelete(bool $reset = true): string * @param integer $limit The limit clause * @param boolean $resetData * - * @return mixed + * @return string|boolean * @throws DatabaseException */ public function delete($where = '', int $limit = null, bool $resetData = true) diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 553727c3385b..94e0dc70fea5 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -612,7 +612,7 @@ abstract protected function execute(string $sql); * @param boolean $setEscapeFlags * @param string $queryClass * - * @return BaseResult|Query|false + * @return BaseResult|Query|boolean * * @todo BC set $queryClass default as null in 4.1 */ @@ -625,7 +625,6 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s $this->initialize(); } - $resultClass = str_replace('Connection', 'Result', get_class($this)); /** * @var Query $query */ @@ -682,7 +681,7 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s Events::trigger('DBQuery', $query); } - return new $resultClass($this->connID, $this->resultID); + return false; } $query->setDuration($startTime); @@ -696,7 +695,20 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s // If $pretend is true, then we just want to return // the actual query object here. There won't be // any results to return. - return $this->pretend ? $query : new $resultClass($this->connID, $this->resultID); + if ($this->pretend) + { + return $query; + } + + // resultID is not false, so it must be successful + if ($this->isWriteType($sql)) + { + return true; + } + + // query is not write-type, so it must be read-type query; return QueryResult + $resultClass = str_replace('Connection', 'Result', get_class($this)); + return new $resultClass($this->connID, $this->resultID); } //-------------------------------------------------------------------- @@ -1755,6 +1767,19 @@ public function resetDataCache() //-------------------------------------------------------------------- + /** + * Determines if the statement is a write-type query or not. + * + * @param string $sql + * @return boolean + */ + 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|MERGE)\s/i', $sql); + } + + //-------------------------------------------------------------------- + /** * Returns the last error code and message. * diff --git a/system/Database/BaseUtils.php b/system/Database/BaseUtils.php index ffb471c6248f..a5b3c440659a 100644 --- a/system/Database/BaseUtils.php +++ b/system/Database/BaseUtils.php @@ -120,7 +120,7 @@ public function databaseExists(string $databaseName): bool * Optimize Table * * @param string $tableName - * @return mixed + * @return boolean * @throws DatabaseException */ public function optimizeTable(string $tableName) @@ -135,13 +135,8 @@ public function optimizeTable(string $tableName) } $query = $this->db->query(sprintf($this->optimizeTable, $this->db->escapeIdentifiers($tableName))); - if ($query !== false) - { - $query = $query->getResultArray(); - return current($query); - } - return false; + return ($query !== false) ? true : false; } //-------------------------------------------------------------------- diff --git a/system/Database/ConnectionInterface.php b/system/Database/ConnectionInterface.php index 32528269a1d2..59e1ba8849da 100644 --- a/system/Database/ConnectionInterface.php +++ b/system/Database/ConnectionInterface.php @@ -116,7 +116,6 @@ public function getPlatform(): string; public function getVersion(): string; //-------------------------------------------------------------------- - /** * Orchestrates a query against the database. Queries must use * Database\Statement objects to store the query and build it. @@ -128,7 +127,7 @@ public function getVersion(): string; * @param string $sql * @param mixed ...$binds * - * @return mixed + * @return BaseResult|Query|boolean */ public function query(string $sql, $binds = null); @@ -193,4 +192,12 @@ public function escape($str); public function callFunction(string $functionName, ...$params); //-------------------------------------------------------------------- + + /** + * Determines if the statement is a write-type query or not. + * + * @param string $sql + * @return boolean + */ + public function isWriteType($sql): bool; } diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index fed38bc2f7f4..010bf4876f2c 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -609,4 +609,24 @@ protected function _transRollback(): bool } // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * Overrides BaseConnection::isWriteType, adding additional read query types. + * + * @param string $sql An SQL query string + * @return boolean + */ + public function isWriteType($sql): bool + { + if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) + { + return false; + } + + return parent::isWriteType($sql); + } + + // -------------------------------------------------------------------- } diff --git a/system/Database/Query.php b/system/Database/Query.php index 97b21f6f5bc3..5f7d943d1374 100644 --- a/system/Database/Query.php +++ b/system/Database/Query.php @@ -280,10 +280,7 @@ public function getErrorMessage(): string */ public function isWriteType(): 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', - $this->originalQueryString - ); + return $this->db->isWriteType($this->originalQueryString); } /** diff --git a/system/Database/SQLSRV/Connection.php b/system/Database/SQLSRV/Connection.php index 92a7fccb9fe1..51a906ed0d58 100755 --- a/system/Database/SQLSRV/Connection.php +++ b/system/Database/SQLSRV/Connection.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Database\SQLSRV; +use CodeIgniter\Database\Query; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; use Exception; @@ -501,17 +502,6 @@ public function execute(string $sql) return $stmt; } - /** - * Determines if a query is a "write" type. - * - * @param string $sql An SQL query string - * @return boolean - */ - public function isWriteType($sql) - { - return (bool) preg_match('/^\s*"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX|MERGE)\s/i', $sql); - } - /** * Returns the last error encountered by this connection. * @@ -578,4 +568,27 @@ public function getVersion(): string return isset($info['SQLServerVersion']) ? $this->dataCache['version'] = $info['SQLServerVersion'] : false; } + + // -------------------------------------------------------------------- + + /** + * Determines if a query is a "write" type. + * + * Overrides BaseConnection::isWriteType, adding additional read query types. + * + * @param string $sql An SQL query string + * @return boolean + */ + public function isWriteType($sql): bool + { + if (preg_match('/^\s*"?(EXEC\s*sp_rename)\s/i', $sql)) + { + return true; + } + + return parent::isWriteType($sql); + } + + // -------------------------------------------------------------------- + } diff --git a/system/Database/SQLite3/Connection.php b/system/Database/SQLite3/Connection.php index d51dc48f475b..3bdf1fa8d18a 100644 --- a/system/Database/SQLite3/Connection.php +++ b/system/Database/SQLite3/Connection.php @@ -11,6 +11,7 @@ namespace CodeIgniter\Database\SQLite3; +use CodeIgniter\Database\Query; use CodeIgniter\Database\BaseConnection; use CodeIgniter\Database\Exceptions\DatabaseException; use ErrorException; @@ -488,20 +489,6 @@ protected function _transRollback(): bool //-------------------------------------------------------------------- - /** - * Determines if the statement is a write-type query or not. - * - * @return boolean - */ - 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. diff --git a/system/Model.php b/system/Model.php index d18a63877116..940a92c70a93 100644 --- a/system/Model.php +++ b/system/Model.php @@ -19,6 +19,7 @@ use CodeIgniter\Database\ConnectionInterface; use CodeIgniter\Database\Exceptions\DatabaseException; use CodeIgniter\Database\Exceptions\DataException; +use CodeIgniter\Database\Query; use CodeIgniter\Exceptions\ModelException; use CodeIgniter\I18n\Time; use CodeIgniter\Validation\ValidationInterface; @@ -253,7 +254,7 @@ protected function doFirst() * * @param array $data Data * - * @return BaseResult|integer|string|false + * @return Query|boolean */ protected function doInsert(array $data) { @@ -278,7 +279,7 @@ protected function doInsert(array $data) $result = $builder->insert(); // If insertion succeeded then save the insert ID - if ($result->resultID) + if ($result) { if (! $this->useAutoIncrement) { @@ -379,7 +380,7 @@ protected function doUpdateBatch(array $set = null, string $index = null, int $b * @param integer|string|array|null $id The rows primary key(s) * @param boolean $purge Allows overriding the soft deletes setting. * - * @return BaseResult|boolean + * @return string|boolean * * @throws DatabaseException */ diff --git a/system/Test/Mock/MockConnection.php b/system/Test/Mock/MockConnection.php index 147e1d1c36d0..9cb660715b7f 100644 --- a/system/Test/Mock/MockConnection.php +++ b/system/Test/Mock/MockConnection.php @@ -48,7 +48,7 @@ public function shouldReturn(string $method, $return) * @param boolean $setEscapeFlags * @param string $queryClass * - * @return BaseResult|Query|false + * @return BaseResult|Query|boolean * * @todo BC set $queryClass default as null in 4.1 */ @@ -81,8 +81,14 @@ public function query(string $sql, $binds = null, bool $setEscapeFlags = true, s $query->setDuration($startTime); - $resultClass = str_replace('Connection', 'Result', get_class($this)); + // resultID is not false, so it must be successful + if ($query->isWriteType()) + { + return true; + } + // query is not write-type, so it must be read-type query; return QueryResult + $resultClass = str_replace('Connection', 'Result', get_class($this)); return new $resultClass($this->connID, $this->resultID); } diff --git a/tests/system/Database/Live/BadQueryTest.php b/tests/system/Database/Live/BadQueryTest.php new file mode 100644 index 000000000000..aacda4194d44 --- /dev/null +++ b/tests/system/Database/Live/BadQueryTest.php @@ -0,0 +1,50 @@ +getPrivateProperty($this->db, 'DBDebug'); + + $this->assertIsBool($this::$origDebug); + } + + public function testBadQueryDebugTrue() + { + // WARNING this value will persist! take care to roll it back. + $this->setPrivateProperty($this->db, 'DBDebug', true); + // expect an exception, class and message varies by DBMS + $this->expectException(\Exception::class); + $query = $this->db->query('SELECT * FROM table_does_not_exist'); + + // this code is never executed + } + + public function testBadQueryDebugFalse() + { + // WARNING this value will persist! take care to roll it back. + $this->setPrivateProperty($this->db, 'DBDebug', false); + // this throws an exception when DBDebug is true, but it'll return FALSE when DBDebug is false + $query = $this->db->query('SELECT * FROM table_does_not_exist'); + $this->assertEquals(false, $query); + + // restore the DBDebug value in effect when this unit test began + $this->setPrivateProperty($this->db, 'DBDebug', self::$origDebug); + } + +} diff --git a/tests/system/Database/Live/DbUtilsTest.php b/tests/system/Database/Live/DbUtilsTest.php index 93cda2a6f109..d04ec575a2cc 100644 --- a/tests/system/Database/Live/DbUtilsTest.php +++ b/tests/system/Database/Live/DbUtilsTest.php @@ -14,6 +14,19 @@ class DbUtilsTest extends CIDatabaseTestCase protected $refresh = true; protected $seed = 'Tests\Support\Database\Seeds\CITestSeeder'; + protected static $origDebug; + + //-------------------------------------------------------------------- + + /** + * This test must run first to store the inital debug value before we tinker with it below + */ + public function testFirst() + { + $this::$origDebug = $this->getPrivateProperty($this->db, 'DBDebug'); + + $this->assertIsBool($this::$origDebug); + } //-------------------------------------------------------------------- @@ -109,16 +122,36 @@ public function testUtilsOptimizeDatabase() //-------------------------------------------------------------------- - public function testUtilsOptimizeTableFalseOptimizeDatabase() + public function testUtilsOptimizeTableFalseOptimizeDatabaseDebugTrue() { $util = (new Database())->loadUtils($this->db); - $this->setPrivateProperty($util, 'optimizeTable', false); + // set debug to true -- WARNING this change will persist! + $this->setPrivateProperty($this->db, 'DBDebug', true); + $this->expectException(DatabaseException::class); $this->expectExceptionMessage('Unsupported feature of the database platform you are using.'); - $util->optimizeDatabase(); + + // this point in code execution will never be reached + } + + //-------------------------------------------------------------------- + + public function testUtilsOptimizeTableFalseOptimizeDatabaseDebugFalse() + { + $util = (new Database())->loadUtils($this->db); + $this->setPrivateProperty($util, 'optimizeTable', false); + + // set debug to false -- WARNING this change will persist! + $this->setPrivateProperty($this->db, 'DBDebug', false); + + $result = $util->optimizeDatabase(); + $this->assertFalse($result); + + // restore original value grabbed from testFirst -- WARNING this change will persist! + $this->setPrivateProperty($this->db, 'DBDebug', self::$origDebug); } //-------------------------------------------------------------------- @@ -129,14 +162,7 @@ public function testUtilsOptimizeTable() $d = $util->optimizeTable('db_job'); - if (in_array($this->db->DBDriver, ['SQLite3', 'Postgre', 'SQLSRV'])) - { - $this->assertFalse((bool) $d); - } - else - { - $this->assertTrue((bool) $d); - } + $this->assertTrue((bool) $d); } //-------------------------------------------------------------------- diff --git a/tests/system/Database/Live/WriteTypeQueryTest.php b/tests/system/Database/Live/WriteTypeQueryTest.php new file mode 100644 index 000000000000..b11c8336d0f1 --- /dev/null +++ b/tests/system/Database/Live/WriteTypeQueryTest.php @@ -0,0 +1,222 @@ +assertTrue($this->db->isWriteType($sql)); + } + + //-------------------------------------------------------------------- + + public function testInsert() + { + $builder = $this->db->table('jobs'); + + $insertData = [ + 'id' => 1, + 'name' => 'Grocery Sales', + ]; + $builder->testMode()->insert($insertData, true); + $sql = $builder->getCompiledInsert(); + + $this->assertTrue($this->db->isWriteType($sql)); + + if ($this->db->DBDriver === 'Postgre') + { + $sql = "INSERT INTO my_table (col1, col2) VALUES ('Joe', 'Cool') RETURNING id;"; + + $this->assertFalse($this->db->isWriteType($sql)); + } + } + + //-------------------------------------------------------------------- + + public function testUpdate() + { + $builder = new BaseBuilder('jobs', $this->db); + $builder->testMode()->where('id', 1)->update(['name' => 'Programmer'], null, null); + $sql = $builder->getCompiledInsert(); + + $this->assertTrue($this->db->isWriteType($sql)); + + if ($this->db->DBDriver === 'Postgre') + { + $sql = "UPDATE my_table SET col1 = 'foo' WHERE id = 2 RETURNING *;"; + + $this->assertFalse($this->db->isWriteType($sql)); + } + } + + //-------------------------------------------------------------------- + + public function testDelete() + { + $builder = $this->db->table('jobs'); + $sql = $builder->testMode()->delete(['id' => 1], null, true); + + $this->assertTrue($this->db->isWriteType($sql)); + } + + //-------------------------------------------------------------------- + + public function testReplace() + { + if (in_array($this->db->DBDriver, ['Postgre', 'SQLSRV'])) + { + // these two were complaining about the builder stuff so i just cooked up this + $sql = 'REPLACE INTO `db_jobs` (`title`, `name`, `date`) VALUES (:title:, :name:, :date:)'; + } + else + { + $builder = $this->db->table('jobs'); + $data = [ + 'title' => 'My title', + 'name' => 'My Name', + 'date' => 'My date', + ]; + $sql = $builder->testMode()->replace($data); + } + + $this->assertTrue($this->db->isWriteType($sql)); + } + + //-------------------------------------------------------------------- + + public function testCreate() + { + $sql = 'CREATE DATABASE foo'; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + //-------------------------------------------------------------------- + + public function testDrop() + { + $sql = 'DROP DATABASE foo'; + + $this->assertTrue($this->db->isWriteType($sql)); + + $sql = 'DROP TABLE foo'; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + //-------------------------------------------------------------------- + + public function testTruncate() + { + $builder = new BaseBuilder('user', $this->db); + $sql = $builder->testMode()->truncate(); + + $this->assertTrue($this->db->isWriteType($sql)); + } + + //-------------------------------------------------------------------- + + public function testLoad() + { + $sql = "LOAD DATA INFILE '/tmp/test.txt' INTO TABLE test FIELDS TERMINATED BY ',' LINES STARTING BY 'xxx';"; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + //-------------------------------------------------------------------- + + public function testCopy() + { + $sql = "COPY demo(firstname, lastname) TO 'demo.txt' DELIMITER ' ';"; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testAlter() + { + $sql = 'ALTER TABLE supplier ADD supplier_name char(50);'; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testRename() + { + if ($this->db->DBDriver === 'SQLSRV') + { + $sql = 'EXEC sp_rename table1 , table2 ;'; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + $sql = 'RENAME ...'; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testGrant() + { + $sql = 'GRANT SELECT ON TABLE my_table TO user1,user2'; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testRevoke() + { + $sql = 'REVOKE SELECT ON TABLE my_table FROM user1,user2'; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testLock() + { + $sql = 'LOCK TABLE my_table IN SHARE MODE;'; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testUnlock() + { + // i think this is only a valid command for MySQL? + $sql = 'UNLOCK TABLES;'; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testReindex() + { + // i think this is only a valid command for Postgre? + $sql = 'REINDEX TABLE foo'; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testSelect() + { + $builder = new BaseBuilder('users', $this->db); + $builder->select('*'); + $sql = $builder->getCompiledSelect(); + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testTrick() + { + $builder = new BaseBuilder('users', $this->db); + $builder->select('UPDATE'); + $sql = $builder->getCompiledSelect(); + + $this->assertFalse($this->db->isWriteType($sql)); + } + +} diff --git a/tests/system/Models/DeleteModelTest.php b/tests/system/Models/DeleteModelTest.php index 38da0970e755..2f30371b9bd9 100644 --- a/tests/system/Models/DeleteModelTest.php +++ b/tests/system/Models/DeleteModelTest.php @@ -16,7 +16,7 @@ public function testDeleteBasics(): void $this->seeInDatabase('job', ['name' => 'Developer']); $result = $this->model->delete(1); - $this->assertTrue($result->resultID !== false); + $this->assertTrue($result); $this->dontSeeInDatabase('job', ['name' => 'Developer']); } @@ -27,7 +27,7 @@ public function testDeleteFail(): void $this->seeInDatabase('job', ['name' => 'Developer']); $result = $this->model->where('name123', 'Developer')->delete(); - $this->assertFalse($result->resultID); + $this->assertFalse($result); $this->seeInDatabase('job', ['name' => 'Developer']); } diff --git a/tests/system/Models/InsertModelTest.php b/tests/system/Models/InsertModelTest.php index 5306bb34e562..ffda633db62c 100644 --- a/tests/system/Models/InsertModelTest.php +++ b/tests/system/Models/InsertModelTest.php @@ -119,7 +119,7 @@ public function testInsertResult(): void $this->createModel(JobModel::class); $result = $this->model->protect(false)->insert($data, false); - $this->assertTrue($result->resultID !== false); + $this->assertTrue($result); $lastInsertId = $this->model->getInsertID(); $this->seeInDatabase('job', ['id' => $lastInsertId]); @@ -136,7 +136,7 @@ public function testInsertResultFail(): void $this->createModel(JobModel::class); $result = $this->model->protect(false)->insert($data, false); - $this->assertFalse($result->resultID); + $this->assertFalse($result); $lastInsertId = $this->model->getInsertID(); $this->assertSame(0, $lastInsertId); diff --git a/user_guide_src/source/changelogs/v4.1.2.rst b/user_guide_src/source/changelogs/v4.1.2.rst index ad891b85cbf9..df3deeb2a3f8 100644 --- a/user_guide_src/source/changelogs/v4.1.2.rst +++ b/user_guide_src/source/changelogs/v4.1.2.rst @@ -24,3 +24,7 @@ Deprecations: - Deprecated cookie-related properties of ``Session`` in order to use the ``Cookie`` class. - Deprecated ``Security::isExpired()`` to use the ``Cookie``'s internal expires status. - Deprecated ``CIDatabaseTestCase`` to use the ``DatabaseTestTrait`` instead. + +Bugs Fixed: + +- ``BaseConnection::query()`` now returns ``false`` for failed queries (unless ``DBDebug==true``, in which case an exception will be thrown) and returns boolean values for write-type queries as specified in the docs. diff --git a/user_guide_src/source/installation/upgrade_412.rst b/user_guide_src/source/installation/upgrade_412.rst new file mode 100644 index 000000000000..07bb8d4e90d4 --- /dev/null +++ b/user_guide_src/source/installation/upgrade_412.rst @@ -0,0 +1,18 @@ +###################################### +Upgrading from 4.1.1 to 4.1.2 +###################################### + +**BaseConnection::query() return values** + +`BaseConnection::query()` method in prior versions was incorrectly returning BaseResult objects +even if the query failed. This method will now return ``false`` for failed queries (or throw an +Exception if ``DBDebug==true``) and will return booleans for write-type queries. Review any use +of ``query()`` method and be assess whether the value might be boolean instead of Result object. +For a better idea of what queries are write-type queries, check ``BaseConnection::isWriteType()`` +and any DBMS-specific override ``isWriteType()`` in the relevant Connection class. + +**ConnectionInterface::isWriteType() declaration added** + +If you have written any classes that implement ConnectionInterface, these must now implement the +``isWriteType()`` method, declared as ``public function isWriteType($sql): bool``. If your class extends BaseConnection, then that class will provide a basic ``isWriteType()`` +method which you might want to override.