-
Notifications
You must be signed in to change notification settings - Fork 2k
feat: add Query Builder exists and doesntExist methods #10215
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 4.8
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1984,6 +1984,105 @@ public function countAll(bool $reset = true) | |
| return (int) $query->numrows; | ||
| } | ||
|
|
||
| /** | ||
| * Determines whether the current Query Builder conditions match any rows. | ||
| * | ||
| * @return bool|string | ||
| */ | ||
| public function exists(bool $reset = true) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Naming-wise, I wonder whether
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about this a bit more, and I still slightly lean toward The main reason is familiarity. So if I were asking "does this query return anything?",
That said, this is just my preference. I'd be happy to follow whatever the team feels is the better fit. |
||
| { | ||
| $exists = $this->doExists($reset); | ||
|
|
||
| return $exists ?? false; | ||
| } | ||
|
|
||
| /** | ||
| * Determines whether the current Query Builder conditions do not match any rows. | ||
| * | ||
| * @return bool|string | ||
| */ | ||
| public function doesntExist(bool $reset = true) | ||
|
memleakd marked this conversation as resolved.
|
||
| { | ||
| $exists = $this->doExists($reset); | ||
|
|
||
| return is_string($exists) ? $exists : $exists === false; | ||
| } | ||
|
|
||
| /** | ||
| * Runs an existence probe for the current Query Builder query. | ||
| * | ||
| * @return bool|string|null | ||
| */ | ||
| protected function doExists(bool $reset = true) | ||
| { | ||
| $sql = $this->compileExists(); | ||
|
|
||
| if ($this->testMode) { | ||
| if ($reset) { | ||
| $this->resetSelect(); | ||
|
|
||
| // Clear our binds so we don't eat up memory | ||
| $this->binds = []; | ||
| } | ||
|
|
||
| return $sql; | ||
| } | ||
|
|
||
| $result = $this->db->query($sql, $this->binds, false); | ||
|
|
||
| if ($reset) { | ||
| $this->resetSelect(); | ||
|
|
||
| // Clear our binds so we don't eat up memory | ||
| $this->binds = []; | ||
| } | ||
|
|
||
| return $result instanceof ResultInterface ? $result->getRow() !== null : null; | ||
| } | ||
|
|
||
| /** | ||
| * Compiles an existence probe for the current Query Builder query. | ||
| */ | ||
| protected function compileExists(): string | ||
| { | ||
| // ORDER BY and FOR UPDATE are unnecessary for checking row existence, | ||
| // and can produce invalid or surprising SQL on some drivers. | ||
| $orderBy = $this->QBOrderBy; | ||
| $limit = $this->QBLimit; | ||
| $offset = $this->QBOffset; | ||
| $lockForUpdate = $this->QBLockForUpdate; | ||
| $select = $this->QBSelect; | ||
| $noEscape = $this->QBNoEscape; | ||
| $needsSubquery = $this->QBUnion !== [] || $this->QBGroupBy !== [] || $this->QBHaving !== [] || $this->QBOffset !== false; | ||
|
|
||
| $this->QBOrderBy = null; | ||
| $this->QBLockForUpdate = false; | ||
|
|
||
| if (! $needsSubquery && $this->QBLimit !== 0) { | ||
| $this->QBLimit = 1; | ||
| } | ||
|
|
||
| try { | ||
| if ($needsSubquery) { | ||
| $sql = "SELECT 1 FROM (\n" . $this->compileSelect() . "\n) CI_exists"; | ||
|
|
||
| $this->QBLimit = 1; | ||
| $this->QBOffset = false; | ||
|
|
||
| return $this->_limit($sql . "\n"); | ||
| } | ||
|
|
||
| return $this->compileSelect('SELECT 1'); | ||
| } finally { | ||
| $this->QBOrderBy = $orderBy; | ||
| $this->QBLimit = $limit; | ||
| $this->QBOffset = $offset; | ||
| $this->QBLockForUpdate = $lockForUpdate; | ||
| $this->QBSelect = $select; | ||
| $this->QBNoEscape = $noEscape; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Generates a platform-specific query string that counts all records | ||
| * returned by an Query Builder query. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * This file is part of CodeIgniter 4 framework. | ||
| * | ||
| * (c) CodeIgniter Foundation <admin@codeigniter.com> | ||
| * | ||
| * For the full copyright and license information, please view | ||
| * the LICENSE file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace CodeIgniter\Database\Builder; | ||
|
|
||
| use CodeIgniter\Database\BaseBuilder; | ||
| use CodeIgniter\Database\SQLSRV\Builder as SQLSRVBuilder; | ||
| use CodeIgniter\Test\CIUnitTestCase; | ||
| use CodeIgniter\Test\Mock\MockConnection; | ||
| use Config\Feature; | ||
| use PHPUnit\Framework\Attributes\Group; | ||
|
|
||
| /** | ||
| * @internal | ||
| */ | ||
| #[Group('Others')] | ||
| final class ExistsTest extends CIUnitTestCase | ||
| { | ||
| protected function setUp(): void | ||
| { | ||
| parent::setUp(); | ||
|
|
||
| $this->db = new MockConnection([]); | ||
| } | ||
|
|
||
| public function testExistsReturnsSqlInTestMode(): void | ||
| { | ||
| $builder = new BaseBuilder('jobs', $this->db); | ||
| $builder->testMode(); | ||
|
|
||
| $answer = $builder->where('id >', 3)->exists(false); | ||
|
|
||
| $expectedSQL = 'SELECT 1 FROM "jobs" WHERE "id" > :id: LIMIT 1'; | ||
|
|
||
| $this->assertSame($expectedSQL, str_replace("\n", ' ', $answer)); | ||
| } | ||
|
|
||
| public function testDoesntExistReturnsSqlInTestMode(): void | ||
| { | ||
| $builder = new BaseBuilder('jobs', $this->db); | ||
| $builder->testMode(); | ||
|
|
||
| $answer = $builder->where('id >', 3)->doesntExist(false); | ||
|
|
||
| $expectedSQL = 'SELECT 1 FROM "jobs" WHERE "id" > :id: LIMIT 1'; | ||
|
|
||
| $this->assertSame($expectedSQL, str_replace("\n", ' ', $answer)); | ||
| } | ||
|
|
||
| public function testExistsDoesNotUseOrderByOrLockForUpdate(): void | ||
| { | ||
| $builder = new BaseBuilder('jobs', $this->db); | ||
| $builder->testMode(); | ||
|
|
||
| $answer = $builder->where('id >', 3) | ||
| ->orderBy('id', 'DESC') | ||
| ->lockForUpdate() | ||
| ->exists(false); | ||
|
|
||
| $expectedSQL = 'SELECT 1 FROM "jobs" WHERE "id" > :id: LIMIT 1'; | ||
|
|
||
| $this->assertSame($expectedSQL, str_replace("\n", ' ', $answer)); | ||
| $this->assertSame( | ||
| 'SELECT * FROM "jobs" WHERE "id" > 3 ORDER BY "id" DESC FOR UPDATE', | ||
| str_replace("\n", ' ', $builder->getCompiledSelect(false)), | ||
| ); | ||
| } | ||
|
|
||
| public function testExistsWithSQLSRVDoesNotUseOrderByOrLockForUpdate(): void | ||
| { | ||
| $this->db = new MockConnection(['DBDriver' => 'SQLSRV', 'database' => 'test', 'schema' => 'dbo']); | ||
|
|
||
| $builder = new SQLSRVBuilder('jobs', $this->db); | ||
| $builder->testMode(); | ||
|
|
||
| $answer = $builder->where('id >', 3) | ||
| ->orderBy('id', 'DESC') | ||
| ->lockForUpdate() | ||
| ->exists(false); | ||
|
|
||
| $expectedSQL = 'SELECT 1 FROM "test"."dbo"."jobs" WHERE "id" > :id: ORDER BY (SELECT NULL) OFFSET 0 ROWS FETCH NEXT 1 ROWS ONLY '; | ||
|
|
||
| $this->assertSame($expectedSQL, str_replace("\n", ' ', $answer)); | ||
| $this->assertSame( | ||
| 'SELECT * FROM "test"."dbo"."jobs" WITH (UPDLOCK, ROWLOCK) WHERE "id" > 3 ORDER BY "id" DESC', | ||
| str_replace("\n", ' ', $builder->getCompiledSelect(false)), | ||
| ); | ||
| } | ||
|
|
||
| public function testExistsHonorsExistingLimitAndOffset(): void | ||
| { | ||
| $builder = new BaseBuilder('jobs', $this->db); | ||
| $builder->testMode(); | ||
|
|
||
| $answer = $builder->where('id >', 3) | ||
| ->limit(10, 20) | ||
| ->exists(false); | ||
|
|
||
| $expectedSQL = 'SELECT 1 FROM ( SELECT * FROM "jobs" WHERE "id" > :id: LIMIT 20, 10 ) CI_exists LIMIT 1'; | ||
|
|
||
| $this->assertSame($expectedSQL, str_replace("\n", ' ', $answer)); | ||
| $this->assertSame( | ||
| 'SELECT * FROM "jobs" WHERE "id" > 3 LIMIT 20, 10', | ||
| str_replace("\n", ' ', $builder->getCompiledSelect(false)), | ||
| ); | ||
| } | ||
|
|
||
| public function testExistsHonorsLimitZero(): void | ||
| { | ||
| $config = config(Feature::class); | ||
| $limitZeroAsAll = $config->limitZeroAsAll; | ||
| $config->limitZeroAsAll = false; | ||
|
|
||
| try { | ||
| $builder = new BaseBuilder('jobs', $this->db); | ||
| $builder->testMode(); | ||
|
|
||
| $answer = $builder->where('id >', 3) | ||
| ->limit(0) | ||
| ->exists(false); | ||
|
|
||
| $expectedSQL = 'SELECT 1 FROM "jobs" WHERE "id" > :id: LIMIT 0'; | ||
|
|
||
| $this->assertSame($expectedSQL, str_replace("\n", ' ', $answer)); | ||
| } finally { | ||
| $config->limitZeroAsAll = $limitZeroAsAll; | ||
| } | ||
| } | ||
|
|
||
| public function testExistsWithGroupByAndHaving(): void | ||
| { | ||
| $builder = new BaseBuilder('jobs', $this->db); | ||
| $builder->testMode(); | ||
|
|
||
| $answer = $builder->selectCount('id', 'total') | ||
| ->where('id >', 3) | ||
| ->groupBy('id') | ||
| ->having('total >', 1) | ||
| ->exists(false); | ||
|
|
||
| $expectedSQL = 'SELECT 1 FROM ( SELECT COUNT("id") AS "total" FROM "jobs" WHERE "id" > :id: GROUP BY "id" HAVING "total" > :total: ) CI_exists LIMIT 1'; | ||
|
|
||
| $this->assertSame($expectedSQL, str_replace("\n", ' ', $answer)); | ||
| $this->assertSame( | ||
| 'SELECT COUNT("id") AS "total" FROM "jobs" WHERE "id" > 3 GROUP BY "id" HAVING "total" > 1', | ||
| str_replace("\n", ' ', $builder->getCompiledSelect(false)), | ||
| ); | ||
| } | ||
|
|
||
| public function testExistsWithUnion(): void | ||
| { | ||
| $builder = new BaseBuilder('jobs', $this->db); | ||
| $builder->testMode(); | ||
|
|
||
| $answer = $builder->union($this->db->table('jobs'))->exists(false); | ||
|
|
||
| $expectedSQL = 'SELECT 1 FROM ( SELECT * FROM (SELECT * FROM "jobs") "uwrp0" UNION SELECT * FROM (SELECT * FROM "jobs") "uwrp1" ) CI_exists LIMIT 1'; | ||
|
|
||
| $this->assertSame($expectedSQL, str_replace("\n", ' ', $answer)); | ||
| $this->assertSame( | ||
| 'SELECT * FROM (SELECT * FROM "jobs") "uwrp0" UNION SELECT * FROM (SELECT * FROM "jobs") "uwrp1"', | ||
| str_replace("\n", ' ', $builder->getCompiledSelect(false)), | ||
| ); | ||
| } | ||
|
|
||
| public function testExistsResetsByDefault(): void | ||
| { | ||
| $builder = new BaseBuilder('jobs', $this->db); | ||
| $builder->testMode(); | ||
|
|
||
| $builder->where('id >', 3)->exists(); | ||
|
|
||
| $this->assertSame('SELECT * FROM "jobs"', str_replace("\n", ' ', $builder->getCompiledSelect(false))); | ||
| $this->assertSame([], $builder->getBinds()); | ||
| } | ||
|
|
||
| public function testExistsHonorsResetFalse(): void | ||
| { | ||
| $builder = new BaseBuilder('jobs', $this->db); | ||
| $builder->testMode(); | ||
|
|
||
| $builder->where('id >', 3)->exists(false); | ||
|
|
||
| $this->assertSame('SELECT * FROM "jobs" WHERE "id" > 3', str_replace("\n", ' ', $builder->getCompiledSelect(false))); | ||
| $this->assertSame([ | ||
| 'id' => [ | ||
| 3, | ||
| true, | ||
| ], | ||
| ], $builder->getBinds()); | ||
| } | ||
|
|
||
| public function testExistsMethodsReturnFalseWhenQueryFails(): void | ||
| { | ||
| $db = new MockConnection([]); | ||
| $db->shouldReturn('execute', false); | ||
|
|
||
| $this->assertFalse((new BaseBuilder('jobs', $db))->exists()); | ||
| $this->assertFalse((new BaseBuilder('jobs', $db))->doesntExist()); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| <?php | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| /** | ||
| * This file is part of CodeIgniter 4 framework. | ||
| * | ||
| * (c) CodeIgniter Foundation <admin@codeigniter.com> | ||
| * | ||
| * For the full copyright and license information, please view | ||
| * the LICENSE file that was distributed with this source code. | ||
| */ | ||
|
|
||
| namespace CodeIgniter\Database\Live; | ||
|
|
||
| use CodeIgniter\Test\CIUnitTestCase; | ||
| use CodeIgniter\Test\DatabaseTestTrait; | ||
| use PHPUnit\Framework\Attributes\Group; | ||
| use Tests\Support\Database\Seeds\CITestSeeder; | ||
|
|
||
| /** | ||
| * @internal | ||
| */ | ||
| #[Group('DatabaseLive')] | ||
| final class ExistsTest extends CIUnitTestCase | ||
| { | ||
| use DatabaseTestTrait; | ||
|
|
||
| protected $refresh = true; | ||
| protected $seed = CITestSeeder::class; | ||
|
|
||
| public function testExistsReturnsTrueWithResults(): void | ||
| { | ||
| $this->assertTrue($this->db->table('job')->where('name', 'Developer')->exists()); | ||
| } | ||
|
|
||
| public function testExistsReturnsFalseWithNoResults(): void | ||
| { | ||
| $this->assertFalse($this->db->table('job')->where('name', 'Superstar')->exists()); | ||
| } | ||
|
|
||
| public function testDoesntExistReturnsFalseWithResults(): void | ||
| { | ||
| $this->assertFalse($this->db->table('job')->where('name', 'Developer')->doesntExist()); | ||
| } | ||
|
|
||
| public function testDoesntExistReturnsTrueWithNoResults(): void | ||
| { | ||
| $this->assertTrue($this->db->table('job')->where('name', 'Superstar')->doesntExist()); | ||
| } | ||
|
|
||
| public function testExistsHonorsReset(): void | ||
| { | ||
| $builder = $this->db->table('job'); | ||
|
|
||
| $this->assertTrue($builder->where('name', 'Developer')->exists(false)); | ||
| $this->assertTrue($builder->exists()); | ||
| } | ||
|
|
||
| public function testExistsHonorsLimitAndOffset(): void | ||
| { | ||
| $this->assertFalse( | ||
| $this->db->table('job') | ||
| ->orderBy('id') | ||
| ->limit(1, 10) | ||
| ->exists(), | ||
| ); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.