diff --git a/system/Database/OCI8/Builder.php b/system/Database/OCI8/Builder.php index abdaf10ae4a1..902852a73713 100644 --- a/system/Database/OCI8/Builder.php +++ b/system/Database/OCI8/Builder.php @@ -200,13 +200,18 @@ protected function _update(string $table, array $values): string protected function _limit(string $sql, bool $offsetIgnore = false): string { $offset = (int) ($offsetIgnore === false ? $this->QBOffset : 0); + if (version_compare($this->db->getVersion(), '12.1', '>=')) { + // OFFSET-FETCH can be used only with the ORDER BY clause + if (empty($this->QBOrderBy)) { + $sql .= ' ORDER BY 1'; + } - // OFFSET-FETCH can be used only with the ORDER BY clause - if (! is_array($this->QBOrderBy) || $this->QBOrderBy === []) { - $sql .= ' ORDER BY 1'; + return $sql . ' OFFSET ' . $offset . ' ROWS FETCH NEXT ' . $this->QBLimit . ' ROWS ONLY'; } - return $sql . ' OFFSET ' . $offset . ' ROWS FETCH NEXT ' . $this->QBLimit . ' ROWS ONLY'; + $limitTemplateQuery = 'SELECT * FROM (SELECT INNER_QUERY.*, ROWNUM RNUM FROM (%s) INNER_QUERY WHERE ROWNUM < %d)' . ($offset ? ' WHERE RNUM >= %d' : ''); + + return sprintf($limitTemplateQuery, $sql, $offset + $this->QBLimit + 1, $offset); } /** diff --git a/tests/system/Database/Builder/LimitTest.php b/tests/system/Database/Builder/LimitTest.php index 4e7bbfd40203..9afdba87d600 100644 --- a/tests/system/Database/Builder/LimitTest.php +++ b/tests/system/Database/Builder/LimitTest.php @@ -14,8 +14,10 @@ namespace CodeIgniter\Database\Builder; use CodeIgniter\Database\BaseBuilder; +use CodeIgniter\Database\OCI8\Builder as OCI8Builder; use CodeIgniter\Test\CIUnitTestCase; use CodeIgniter\Test\Mock\MockConnection; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; /** @@ -65,4 +67,117 @@ public function testLimitAndOffsetMethod(): void $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); } + + #[DataProvider('provideOracle11gVersions')] + public function testLimitAloneWithOracle11g(string $version): void + { + $builder = new OCI8Builder('user', $this->createOci8Connection($version)); + + $builder->limit(5); + + $expectedSQL = 'SELECT * FROM (SELECT INNER_QUERY.*, ROWNUM RNUM FROM (SELECT * FROM "user" ) INNER_QUERY WHERE ROWNUM < 6)'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + #[DataProvider('provideOracle11gVersions')] + public function testLimitAndOffsetWithOracle11g(string $version): void + { + $builder = new OCI8Builder('user', $this->createOci8Connection($version)); + + $builder->limit(5, 1); + + $expectedSQL = 'SELECT * FROM (SELECT INNER_QUERY.*, ROWNUM RNUM FROM (SELECT * FROM "user" ) INNER_QUERY WHERE ROWNUM < 7) WHERE RNUM >= 1'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + #[DataProvider('provideOracle11gVersions')] + public function testLimitAndOffsetWithOrderByOracle11g(string $version): void + { + $builder = new OCI8Builder('user', $this->createOci8Connection($version)); + + $builder->orderBy('id', 'ASC')->limit(5, 1); + + $expectedSQL = 'SELECT * FROM (SELECT INNER_QUERY.*, ROWNUM RNUM FROM (SELECT * FROM "user" ORDER BY "id" ASC ) INNER_QUERY WHERE ROWNUM < 7) WHERE RNUM >= 1'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + #[DataProvider('provideOracle12_1AndAboveVersions')] + public function testLimitAloneWithOracle12_1AndAbove(string $version): void + { + $builder = new OCI8Builder('user', $this->createOci8Connection($version)); + + $builder->limit(5); + + $expectedSQL = 'SELECT * FROM "user" ORDER BY 1 OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + #[DataProvider('provideOracle12_1AndAboveVersions')] + public function testLimitAndOffsetWithOracle12_1AndAbove(string $version): void + { + $builder = new OCI8Builder('user', $this->createOci8Connection($version)); + + $builder->limit(5, 1); + + $expectedSQL = 'SELECT * FROM "user" ORDER BY 1 OFFSET 1 ROWS FETCH NEXT 5 ROWS ONLY'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + #[DataProvider('provideOracle12_1AndAboveVersions')] + public function testLimitAndOffsetWithOrderByOracle12_1AndAbove(string $version): void + { + $builder = new OCI8Builder('user', $this->createOci8Connection($version)); + + $builder->orderBy('id', 'ASC')->limit(5, 1); + + $expectedSQL = 'SELECT * FROM "user" ORDER BY "id" ASC OFFSET 1 ROWS FETCH NEXT 5 ROWS ONLY'; + + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); + } + + /** + * @return iterable + */ + public static function provideOracle11gVersions(): iterable + { + yield '11.2' => ['11.2.0.4.0']; + + yield '11.1' => ['11.1.0.7.0']; + } + + /** + * @return iterable + */ + public static function provideOracle12_1AndAboveVersions(): iterable + { + yield '12.1' => ['12.1']; + + yield '12.1.0.2' => ['12.1.0.2.0']; + + yield '19c' => ['19.0.0.0.0']; + } + + private function createOci8Connection(string $version): MockConnection + { + return new class (['DBDriver' => 'OCI8'], $version) extends MockConnection { + private readonly string $oracleVersion; + + public function __construct(array $params, string $version) + { + parent::__construct($params); + + $this->oracleVersion = $version; + } + + public function getVersion(): string + { + return $this->oracleVersion; + } + }; + } }