|
14 | 14 | namespace CodeIgniter\Database\Builder; |
15 | 15 |
|
16 | 16 | use CodeIgniter\Database\BaseBuilder; |
| 17 | +use CodeIgniter\Database\Exceptions\DatabaseException; |
17 | 18 | use CodeIgniter\Database\RawSql; |
18 | 19 | use CodeIgniter\Exceptions\InvalidArgumentException; |
19 | 20 | use CodeIgniter\Test\CIUnitTestCase; |
@@ -489,6 +490,141 @@ public static function provideWhereColumnInvalidColumnThrowInvalidArgumentExcept |
489 | 490 | ]; |
490 | 491 | } |
491 | 492 |
|
| 493 | + public function testWhereExistsSubQuery(): void |
| 494 | + { |
| 495 | + $expectedSQL = 'SELECT * FROM "users" WHERE EXISTS (SELECT 1 FROM "orders" WHERE "orders"."user_id" = "users"."id")'; |
| 496 | + |
| 497 | + // Closure |
| 498 | + $builder = $this->db->table('users'); |
| 499 | + |
| 500 | + $builder->whereExists(static fn (BaseBuilder $builder) => $builder |
| 501 | + ->select('1', false) |
| 502 | + ->from('orders') |
| 503 | + ->whereColumn('orders.user_id', 'users.id')); |
| 504 | + |
| 505 | + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); |
| 506 | + |
| 507 | + // Builder |
| 508 | + $builder = $this->db->table('users'); |
| 509 | + |
| 510 | + $subQuery = $this->db->table('orders') |
| 511 | + ->select('1', false) |
| 512 | + ->whereColumn('orders.user_id', 'users.id'); |
| 513 | + |
| 514 | + $builder->whereExists($subQuery); |
| 515 | + |
| 516 | + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); |
| 517 | + } |
| 518 | + |
| 519 | + #[DataProvider('provideWhereExistsVariants')] |
| 520 | + public function testWhereExistsVariants(string $method, string $expectedSQL): void |
| 521 | + { |
| 522 | + $builder = $this->db->table('users'); |
| 523 | + |
| 524 | + $builder->where('active', 1); |
| 525 | + |
| 526 | + $builder->{$method}(static fn (BaseBuilder $builder) => $builder |
| 527 | + ->select('1', false) |
| 528 | + ->from('orders') |
| 529 | + ->whereColumn('orders.user_id', 'users.id')); |
| 530 | + |
| 531 | + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); |
| 532 | + } |
| 533 | + |
| 534 | + /** |
| 535 | + * @return iterable<string, array{string, string}> |
| 536 | + */ |
| 537 | + public static function provideWhereExistsVariants(): iterable |
| 538 | + { |
| 539 | + $exists = '(SELECT 1 FROM "orders" WHERE "orders"."user_id" = "users"."id")'; |
| 540 | + $baseQuery = 'SELECT * FROM "users" WHERE "active" = 1'; |
| 541 | + |
| 542 | + return [ |
| 543 | + 'whereExists' => ['whereExists', "{$baseQuery} AND EXISTS {$exists}"], |
| 544 | + 'orWhereExists' => ['orWhereExists', "{$baseQuery} OR EXISTS {$exists}"], |
| 545 | + 'whereNotExists' => ['whereNotExists', "{$baseQuery} AND NOT EXISTS {$exists}"], |
| 546 | + 'orWhereNotExists' => ['orWhereNotExists', "{$baseQuery} OR NOT EXISTS {$exists}"], |
| 547 | + ]; |
| 548 | + } |
| 549 | + |
| 550 | + public function testWhereExistsWithGroupedConditions(): void |
| 551 | + { |
| 552 | + $builder = $this->db->table('users'); |
| 553 | + |
| 554 | + $builder->groupStart() |
| 555 | + ->whereExists(static fn (BaseBuilder $builder) => $builder |
| 556 | + ->select('1', false) |
| 557 | + ->from('orders') |
| 558 | + ->whereColumn('orders.user_id', 'users.id')) |
| 559 | + ->orWhereNotExists(static fn (BaseBuilder $builder) => $builder |
| 560 | + ->select('1', false) |
| 561 | + ->from('jobs') |
| 562 | + ->whereColumn('jobs.user_id', 'users.id')) |
| 563 | + ->groupEnd() |
| 564 | + ->where('active', 1); |
| 565 | + |
| 566 | + $expectedSQL = 'SELECT * FROM "users" WHERE ( EXISTS (SELECT 1 FROM "orders" WHERE "orders"."user_id" = "users"."id") OR NOT EXISTS (SELECT 1 FROM "jobs" WHERE "jobs"."user_id" = "users"."id") ) AND "active" = 1'; |
| 567 | + |
| 568 | + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); |
| 569 | + } |
| 570 | + |
| 571 | + public function testWhereExistsWithOuterAndInnerBinds(): void |
| 572 | + { |
| 573 | + $builder = $this->db->table('users'); |
| 574 | + |
| 575 | + $builder->where('active', 1) |
| 576 | + ->whereExists(static fn (BaseBuilder $builder) => $builder |
| 577 | + ->select('1', false) |
| 578 | + ->from('orders') |
| 579 | + ->where('orders.status', 'paid') |
| 580 | + ->whereColumn('orders.user_id', 'users.id')); |
| 581 | + |
| 582 | + $expectedSQL = 'SELECT * FROM "users" WHERE "active" = 1 AND EXISTS (SELECT 1 FROM "orders" WHERE "orders"."status" = \'paid\' AND "orders"."user_id" = "users"."id")'; |
| 583 | + $expectedBinds = [ |
| 584 | + 'active' => [ |
| 585 | + 1, |
| 586 | + true, |
| 587 | + ], |
| 588 | + ]; |
| 589 | + |
| 590 | + $this->assertSame($expectedSQL, str_replace("\n", ' ', $builder->getCompiledSelect())); |
| 591 | + $this->assertSame($expectedBinds, $builder->getBinds()); |
| 592 | + } |
| 593 | + |
| 594 | + /** |
| 595 | + * @param mixed $subquery |
| 596 | + */ |
| 597 | + #[DataProvider('provideWhereExistsInvalidSubqueryThrowInvalidArgumentException')] |
| 598 | + public function testWhereExistsInvalidSubqueryThrowInvalidArgumentException($subquery): void |
| 599 | + { |
| 600 | + $this->expectException(InvalidArgumentException::class); |
| 601 | + |
| 602 | + $builder = $this->db->table('users'); |
| 603 | + $builder->whereExists($subquery); |
| 604 | + } |
| 605 | + |
| 606 | + /** |
| 607 | + * @return iterable<string, array{mixed}> |
| 608 | + */ |
| 609 | + public static function provideWhereExistsInvalidSubqueryThrowInvalidArgumentException(): iterable |
| 610 | + { |
| 611 | + return [ |
| 612 | + 'null' => [null], |
| 613 | + 'array' => [[]], |
| 614 | + 'stdClass' => [new stdClass()], |
| 615 | + 'raw string' => ['SELECT 1'], |
| 616 | + ]; |
| 617 | + } |
| 618 | + |
| 619 | + public function testWhereExistsSameBaseBuilderObject(): void |
| 620 | + { |
| 621 | + $this->expectException(DatabaseException::class); |
| 622 | + $this->expectExceptionMessage('The subquery cannot be the same object as the main query object.'); |
| 623 | + |
| 624 | + $builder = $this->db->table('users'); |
| 625 | + $builder->whereExists($builder); |
| 626 | + } |
| 627 | + |
492 | 628 | public function testWhereIn(): void |
493 | 629 | { |
494 | 630 | $builder = $this->db->table('jobs'); |
|
0 commit comments