diff --git a/system/Database/BaseConnection.php b/system/Database/BaseConnection.php index 6e6411ff64a4..6be66bbbb39d 100644 --- a/system/Database/BaseConnection.php +++ b/system/Database/BaseConnection.php @@ -1686,7 +1686,7 @@ public function resetDataCache() */ 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); + return (bool) preg_match('/^\s*(WITH\s.+(\s|[)]))?"?(SET|INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|TRUNCATE|LOAD|COPY|ALTER|RENAME|GRANT|REVOKE|LOCK|UNLOCK|REINDEX|MERGE)\s(?!.*\sRETURNING\s)/is', $sql); } /** diff --git a/system/Database/Postgre/Connection.php b/system/Database/Postgre/Connection.php index fecf330d7407..9974e43e1e90 100644 --- a/system/Database/Postgre/Connection.php +++ b/system/Database/Postgre/Connection.php @@ -569,20 +569,4 @@ protected function _transRollback(): bool { return (bool) pg_query($this->connID, 'ROLLBACK'); } - - /** - * Determines if a query is a "write" type. - * - * Overrides BaseConnection::isWriteType, adding additional read query types. - * - * @param string $sql - */ - public function isWriteType($sql): bool - { - if (preg_match('#^(INSERT|UPDATE).*RETURNING\s.+(\,\s?.+)*$#is', $sql)) { - return false; - } - - return parent::isWriteType($sql); - } } diff --git a/tests/system/Database/Live/WriteTypeQueryTest.php b/tests/system/Database/Live/WriteTypeQueryTest.php index 3c250080e0b8..56b700e76fbc 100644 --- a/tests/system/Database/Live/WriteTypeQueryTest.php +++ b/tests/system/Database/Live/WriteTypeQueryTest.php @@ -37,7 +37,7 @@ public function testSet(): void $this->assertTrue($this->db->isWriteType($sql)); } - public function testInsert(): void + public function testInsertBuilder(): void { $builder = $this->db->table('jobs'); @@ -49,30 +49,198 @@ public function testInsert(): void $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;"; + public function testInsertOne(): void + { + $sql = "INSERT INTO my_table (col1, col2) VALUES ('Joe', 'Cool');"; - $this->assertFalse($this->db->isWriteType($sql)); - } + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testInsertMulti(): void + { + $sql = <<<'SQL' + INSERT INTO my_table (col1, col2) + VALUES ('Joe', 'Cool'); + SQL; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testInsertWithOne(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval) INSERT INTO my_table (col1, col2) SELECT 'Joe', seqval FROM seqvals;"; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testInsertWithOneNoSpace(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval)INSERT INTO my_table (col1, col2) SELECT 'Joe', seqval FROM seqvals;"; + + $this->assertTrue($this->db->isWriteType($sql)); } - public function testUpdate(): void + public function testInsertWithMulti(): void + { + $sql = <<<'SQL' + WITH seqvals AS (SELECT '3' AS seqval) + INSERT INTO my_table (col1, col2) + SELECT 'Joe', seqval + FROM seqvals; + SQL; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testInsertOneReturning(): void + { + $sql = "INSERT INTO my_table (col1, col2) VALUES ('Joe', 'Cool') RETURNING id;"; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testInsertMultiReturning(): void + { + $sql = <<<'SQL' + INSERT INTO my_table (col1, col2) + VALUES ('Joe', 'Cool') + RETURNING id; + SQL; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testInsertWithOneReturning(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval) INSERT INTO my_table (col1, col2) SELECT 'Joe', seqval FROM seqvals RETURNING id;"; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testInsertWithOneReturningNoSpace(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval)INSERT INTO my_table (col1, col2) SELECT 'Joe', seqval FROM seqvals RETURNING id;"; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testInsertWithMultiReturning(): void + { + $sql = <<<'SQL' + WITH seqvals AS (SELECT '3' AS seqval) + INSERT INTO my_table (col1, col2) + SELECT 'Joe', seqval + FROM seqvals + RETURNING id; + SQL; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testUpdateBuilder(): void { $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 *;"; + public function testUpdateOne(): void + { + $sql = "UPDATE my_table SET col1 = 'foo' WHERE id = 2;"; - $this->assertFalse($this->db->isWriteType($sql)); - } + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testUpdateMulti(): void + { + $sql = <<<'SQL' + UPDATE my_table + SET col1 = 'foo' + WHERE id = 2; + SQL; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testUpdateWithOne(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval) UPDATE my_table SET col1 = seqval FROM seqvals WHERE id = 2;"; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testUpdateWithOneNoSpace(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval)UPDATE my_table SET col1 = seqval FROM seqvals WHERE id = 2;"; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testUpdateWithMulti(): void + { + $sql = <<<'SQL' + WITH seqvals AS (SELECT '3' AS seqval) + UPDATE my_table + SET col1 = seqval + FROM seqvals + WHERE id = 2; + SQL; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testUpdateOneReturning(): void + { + $sql = "UPDATE my_table SET col1 = 'foo' WHERE id = 2 RETURNING *;"; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testUpdateMultiReturning(): void + { + $sql = <<<'SQL' + UPDATE my_table + SET col1 = 'foo' + WHERE id = 2 + RETURNING *; + SQL; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testUpdateWithOneReturning(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval) UPDATE my_table SET col1 = seqval FROM seqvals WHERE id = 2 RETURNING *;"; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testUpdateWithOneReturningNoSpace(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval)UPDATE my_table SET col1 = seqval FROM seqvals WHERE id = 2 RETURNING *;"; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testUpdateWithMultiReturning(): void + { + $sql = <<<'SQL' + WITH seqvals AS (SELECT '3' AS seqval) + UPDATE my_table + SET col1 = seqval + FROM seqvals + WHERE id = 2 + RETURNING *; + SQL; + + $this->assertFalse($this->db->isWriteType($sql)); } - public function testDelete(): void + public function testDeleteBuilder(): void { $builder = $this->db->table('jobs'); $sql = $builder->testMode()->delete(['id' => 1], null, true); @@ -80,6 +248,96 @@ public function testDelete(): void $this->assertTrue($this->db->isWriteType($sql)); } + public function testDeleteOne(): void + { + $sql = 'DELETE FROM my_table WHERE id = 2;'; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testDeleteMulti(): void + { + $sql = <<<'SQL' + DELETE FROM my_table + WHERE id = 2; + SQL; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testDeleteWithOne(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval) DELETE FROM my_table JOIN seqvals ON col1 = seqval;"; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testDeleteWithOneNoSpace(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval)DELETE FROM my_table JOIN seqvals ON col1 = seqval;"; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testDeleteWithMulti(): void + { + $sql = <<<'SQL' + WITH seqvals AS + (SELECT '3' AS seqval) + DELETE FROM my_table + JOIN seqvals + ON col1 = seqval; + SQL; + + $this->assertTrue($this->db->isWriteType($sql)); + } + + public function testDeleteOneReturning(): void + { + $sql = 'DELETE FROM my_table WHERE id = 2 RETURNING *;'; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testDeleteMultiReturning(): void + { + $sql = <<<'SQL' + DELETE FROM my_table + WHERE id = 2 + RETURNING *; + SQL; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testDeleteWithOneReturning(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval) DELETE FROM my_table JOIN seqvals ON col1 = seqval RETURNING *;"; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testDeleteWithOneReturningNoSpace(): void + { + $sql = "WITH seqvals AS (SELECT '3' AS seqval)DELETE FROM my_table JOIN seqvals ON col1 = seqval RETURNING *;"; + + $this->assertFalse($this->db->isWriteType($sql)); + } + + public function testDeleteWithMultiReturning(): void + { + $sql = <<<'SQL' + WITH seqvals AS + (SELECT '3' AS seqval) + DELETE FROM my_table + JOIN seqvals + ON col1 = seqval + RETURNING *; + SQL; + + $this->assertFalse($this->db->isWriteType($sql)); + } + public function testReplace(): void { if (in_array($this->db->DBDriver, ['Postgre', 'SQLSRV'], true)) { @@ -98,19 +356,22 @@ public function testReplace(): void $this->assertTrue($this->db->isWriteType($sql)); } - public function testCreate(): void + public function testCreateDatabase(): void { $sql = 'CREATE DATABASE foo'; $this->assertTrue($this->db->isWriteType($sql)); } - public function testDrop(): void + public function testDropDatabase(): void { $sql = 'DROP DATABASE foo'; $this->assertTrue($this->db->isWriteType($sql)); + } + public function testDropTable(): void + { $sql = 'DROP TABLE foo'; $this->assertTrue($this->db->isWriteType($sql)); diff --git a/tests/system/I18n/TimeLegacyTest.php b/tests/system/I18n/TimeLegacyTest.php index 674d9b11dab0..8f0fec545e47 100644 --- a/tests/system/I18n/TimeLegacyTest.php +++ b/tests/system/I18n/TimeLegacyTest.php @@ -401,10 +401,15 @@ public function testGetTimestamp(): void public function testGetAge(): void { + // setTestNow() does not work to parse(). $time = TimeLegacy::parse('5 years ago'); - $this->assertSame(5, $time->getAge()); - $this->assertSame(5, $time->age); + // Considers leap year + $now = TimeLegacy::now(); + $expected = ($now->day === '29' && $now->month === '2') ? 4 : 5; + + $this->assertSame($expected, $time->getAge()); + $this->assertSame($expected, $time->age); } public function testAgeNow(): void @@ -416,7 +421,7 @@ public function testAgeNow(): void public function testAgeFuture(): void { - TimeLegacy::setTestNow('June 20, 2022', 'America/Chicago'); + TimeLegacy::setTestNow('June 20, 2022'); $time = TimeLegacy::parse('August 12, 2116 4:15:23pm'); $this->assertSame(0, $time->getAge()); @@ -424,7 +429,7 @@ public function testAgeFuture(): void public function testGetAgeSameDayOfBirthday(): void { - TimeLegacy::setTestNow('December 31, 2022', 'America/Chicago'); + TimeLegacy::setTestNow('December 31, 2022'); $time = TimeLegacy::parse('December 31, 2020'); $this->assertSame(2, $time->getAge()); @@ -432,7 +437,7 @@ public function testGetAgeSameDayOfBirthday(): void public function testGetAgeNextDayOfBirthday(): void { - TimeLegacy::setTestNow('January 1, 2022', 'America/Chicago'); + TimeLegacy::setTestNow('January 1, 2022'); $time = TimeLegacy::parse('December 31, 2020'); $this->assertSame(1, $time->getAge()); @@ -440,7 +445,7 @@ public function testGetAgeNextDayOfBirthday(): void public function testGetAgeBeforeDayOfBirthday(): void { - TimeLegacy::setTestNow('December 30, 2021', 'America/Chicago'); + TimeLegacy::setTestNow('December 30, 2021'); $time = TimeLegacy::parse('December 31, 2020'); $this->assertSame(0, $time->getAge()); diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php index 51b4cafc502d..b81c8ae299af 100644 --- a/tests/system/I18n/TimeTest.php +++ b/tests/system/I18n/TimeTest.php @@ -408,10 +408,15 @@ public function testGetTimestamp(): void */ public function testGetAge(): void { + // setTestNow() does not work to parse(). $time = Time::parse('5 years ago'); - $this->assertSame(5, $time->getAge()); - $this->assertSame(5, $time->age); + // Considers leap year + $now = Time::now(); + $expected = ($now->day === '29' && $now->month === '2') ? 4 : 5; + + $this->assertSame($expected, $time->getAge()); + $this->assertSame($expected, $time->age); } public function testAgeNow(): void @@ -423,7 +428,7 @@ public function testAgeNow(): void public function testAgeFuture(): void { - Time::setTestNow('June 20, 2022', 'America/Chicago'); + Time::setTestNow('June 20, 2022'); $time = Time::parse('August 12, 2116 4:15:23pm'); $this->assertSame(0, $time->getAge()); @@ -431,7 +436,7 @@ public function testAgeFuture(): void public function testGetAgeSameDayOfBirthday(): void { - Time::setTestNow('December 31, 2022', 'America/Chicago'); + Time::setTestNow('December 31, 2022'); $time = Time::parse('December 31, 2020'); $this->assertSame(2, $time->getAge()); @@ -439,7 +444,7 @@ public function testGetAgeSameDayOfBirthday(): void public function testGetAgeNextDayOfBirthday(): void { - Time::setTestNow('January 1, 2022', 'America/Chicago'); + Time::setTestNow('January 1, 2022'); $time = Time::parse('December 31, 2020'); $this->assertSame(1, $time->getAge()); @@ -447,7 +452,7 @@ public function testGetAgeNextDayOfBirthday(): void public function testGetAgeBeforeDayOfBirthday(): void { - Time::setTestNow('December 30, 2021', 'America/Chicago'); + Time::setTestNow('December 30, 2021'); $time = Time::parse('December 31, 2020'); $this->assertSame(0, $time->getAge()); diff --git a/user_guide_src/source/models/entities.rst b/user_guide_src/source/models/entities.rst index 6e683dcafe0b..bcc0c2ec57d0 100644 --- a/user_guide_src/source/models/entities.rst +++ b/user_guide_src/source/models/entities.rst @@ -60,12 +60,21 @@ Create the model first at **app/Models/UserModel.php** so that we can interact w .. literalinclude:: entities/002.php -The model uses the ``users`` table in the database for all of its activities. We've set the ``$allowedFields`` property -to include all of the fields that we want outside classes to change. The ``id``, ``created_at``, and ``updated_at`` fields -are handled automatically by the class or the database, so we don't want to change those. Finally, we've set our Entity -class as the ``$returnType``. This ensures that all methods on the model that return rows from the database will return +The model uses the ``users`` table in the database for all of its activities. + +We've set the ``$allowedFields`` property to include all of the fields that we +want outside classes to change. The ``id``, ``created_at``, and ``updated_at`` +fields are handled automatically by the class or the database, so we don't want +to change those. + +Finally, we've set our Entity class as the ``$returnType``. This ensures that all +built-in methods on the model that return rows from the database will return instances of our User Entity class instead of an object or array like normal. +.. note:: + Of course, if you add a custom method to your model, you must implement it + so that instances of ``$returnType`` are returned. + Working with the Entity Class ============================= diff --git a/user_guide_src/source/models/model.rst b/user_guide_src/source/models/model.rst index a175d7576fe8..a7eebb4ba6d2 100644 --- a/user_guide_src/source/models/model.rst +++ b/user_guide_src/source/models/model.rst @@ -123,12 +123,16 @@ default value is ``true``. $returnType ----------- -The Model's CRUD methods will take a step of work away from you and automatically return -the resulting data, instead of the Result object. This setting allows you to define -the type of data that is returned. Valid values are '**array**' (the default), '**object**', or the **fully -qualified name of a class** that can be used with the Result object's ``getCustomResultObject()`` -method. Using the special ``::class`` constant of the class will allow most IDEs to -auto-complete the name and allow functions like refactoring to better understand your code. +The Model's **find*()** methods will take a step of work away from you and automatically +return the resulting data, instead of the Result object. + +This setting allows you to define the type of data that is returned. Valid values +are '**array**' (the default), '**object**', or the **fully qualified name of a class** +that can be used with the Result object's ``getCustomResultObject()`` method. + +Using the special ``::class`` constant of the class will allow most IDEs to +auto-complete the name and allow functions like refactoring to better understand +your code. .. _model-use-soft-deletes: @@ -145,7 +149,7 @@ This requires either a DATETIME or INTEGER field in the database as per the mode `$dateFormat`_ setting. The default field name is ``deleted_at`` however this name can be configured to any name of your choice by using `$deletedField`_ property. -.. important:: The ``deleted_at`` field must be nullable. +.. important:: The ``deleted_at`` field in the database must be nullable. $allowedFields --------------