From f1dd03e5d5026da8e781637e75a57fdcf3245c5c Mon Sep 17 00:00:00 2001 From: joshuadave143 Date: Thu, 29 Oct 2026 15:55:56 +0800 Subject: [PATCH 1/2] fix: restore backward compatibility for Oracle pagination Co-authored-by: Cursor --- system/Database/OCI8/Builder.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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); } /** From 4153a7488dc62267a5185de23baaa979ea70f787 Mon Sep 17 00:00:00 2001 From: joshuadave143 Date: Thu, 29 Oct 2026 15:53:40 +0800 Subject: [PATCH 2/2] test: cover OCI8 limit SQL for Oracle 11g and 12.1+ Co-authored-by: Cursor --- tests/system/Database/Builder/LimitTest.php | 115 ++++++++++++++++++++ 1 file changed, 115 insertions(+) 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; + } + }; + } }