diff --git a/CHANGELOG.md b/CHANGELOG.md index 930292b21..0aa2db725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ - New #408, #410: Implement `DMLQueryBuilder::upsertReturning()` method (@Tigrov) - Enh #412: Reduce binding parameters (@Tigrov) - Chg #414: Rename `DMLQueryBuilder::insertWithReturningPks()` to `DMLQueryBuilder::insertReturningPks()` (@Tigrov) +- Enh #415: Implement `CaseExpressionBuilder` class (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/src/Builder/CaseExpressionBuilder.php b/src/Builder/CaseExpressionBuilder.php new file mode 100644 index 000000000..10ac63d18 --- /dev/null +++ b/src/Builder/CaseExpressionBuilder.php @@ -0,0 +1,65 @@ +getWhen(); + + if (empty($whenClauses)) { + throw new InvalidArgumentException('The CASE expression must have at least one WHEN clause.'); + } + + $sql = 'CASE'; + + $case = $expression->getCase(); + + if ($case !== null) { + $caseTypeHint = $this->buildTypeHint($expression->getCaseType()); + $sql .= ' ' . $this->buildConditionWithTypeHint($case, $caseTypeHint, $params); + } else { + $caseTypeHint = ''; + } + + foreach ($whenClauses as $when) { + $sql .= ' WHEN ' . $this->buildConditionWithTypeHint($when->condition, $caseTypeHint, $params); + $sql .= ' THEN ' . $this->buildResult($when->result, $params); + } + + if ($expression->hasElse()) { + $sql .= ' ELSE ' . $this->buildResult($expression->getElse(), $params); + } + + return $sql . ' END'; + } + + private function buildConditionWithTypeHint(mixed $condition, string $typeHint, array &$params): string + { + $builtCondition = $this->buildCondition($condition, $params); + + return $typeHint !== '' ? "($builtCondition)$typeHint" : $builtCondition; + } + + private function buildTypeHint(string|ColumnInterface $type): string + { + if (is_string($type)) { + return $type === '' ? '' : "::$type"; + } + + return '::' . $this->queryBuilder->getColumnDefinitionBuilder()->buildType($type); + } +} diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index 3c698e980..2b37f166c 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -5,10 +5,12 @@ namespace Yiisoft\Db\Pgsql; use Yiisoft\Db\Expression\ArrayExpression; +use Yiisoft\Db\Expression\CaseExpression; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Expression\StructuredExpression; use Yiisoft\Db\Pgsql\Builder\ArrayExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\ArrayOverlapsConditionBuilder; +use Yiisoft\Db\Pgsql\Builder\CaseExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\JsonOverlapsConditionBuilder; use Yiisoft\Db\Pgsql\Builder\LikeConditionBuilder; use Yiisoft\Db\Pgsql\Builder\StructuredExpressionBuilder; @@ -51,6 +53,7 @@ protected function defaultExpressionBuilders(): array JsonOverlapsCondition::class => JsonOverlapsConditionBuilder::class, StructuredExpression::class => StructuredExpressionBuilder::class, LikeCondition::class => LikeConditionBuilder::class, + CaseExpression::class => CaseExpressionBuilder::class, ]; } } diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 928efb236..e1fb60b81 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -8,8 +8,10 @@ use Yiisoft\Db\Constant\DataType; use Yiisoft\Db\Constant\PseudoType; use Yiisoft\Db\Expression\ArrayExpression; +use Yiisoft\Db\Expression\CaseExpression; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Pgsql\Column\ColumnBuilder; +use Yiisoft\Db\Pgsql\Column\IntegerColumn; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; @@ -511,4 +513,60 @@ public static function prepareValue(): array return $values; } + + public static function caseExpressionBuilder(): array + { + $data = parent::caseExpressionBuilder(); + + $db = self::getDb(); + $serverVersion = $db->getServerInfo()->getVersion(); + $db->close(); + + if (version_compare($serverVersion, '10', '<')) { + $data['without case expression'] = [ + (new CaseExpression()) + ->addWhen(['=', 'column_name', 1], $paramA = new Param('a', DataType::STRING)) + ->addWhen( + '"column_name" = 2', + $db->select(new Expression( + ':pv2::text', + [':pv2' => $paramB = new Param('b', DataType::STRING)], + )), + ), + 'CASE WHEN "column_name" = :qp0 THEN :qp1 WHEN "column_name" = 2 THEN (SELECT :pv2::text) END', + [':qp0' => 1, ':qp1' => $paramA, ':pv2' => $paramB], + 'b', + ]; + } + + return [ + ...$data, + 'without case and type hint' => [ + (new CaseExpression())->caseType('int') + ->addWhen(true, "'a'"), + "CASE WHEN TRUE THEN 'a' END", + [], + 'a', + ], + 'with case and type hint' => [ + (new CaseExpression('1 + 1', 'int')) + ->addWhen(1, "'a'") + ->else("'b'"), + "CASE (1 + 1)::int WHEN (1)::int THEN 'a' ELSE 'b' END", + [], + 'b', + ], + 'with case and type hint with column' => [ + (new CaseExpression('1 + 1', new IntegerColumn())) + ->addWhen(1, $paramA = new Param('a', DataType::STRING)) + ->else($paramB = new Param('b', DataType::STRING)), + 'CASE (1 + 1)::integer WHEN (1)::integer THEN :qp0 ELSE :qp1 END', + [ + ':qp0' => $paramA, + ':qp1' => $paramB, + ], + 'b', + ], + ]; + } } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 10b422f72..469e93ac0 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -8,6 +8,7 @@ use Yiisoft\Db\Driver\Pdo\PdoConnectionInterface; use Yiisoft\Db\Exception\IntegrityException; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Expression\CaseExpression; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider; @@ -576,4 +577,14 @@ public function testPrepareValue(string $expected, mixed $value): void { parent::testPrepareValue($expected, $value); } + + #[DataProviderExternal(QueryBuilderProvider::class, 'caseExpressionBuilder')] + public function testCaseExpressionBuilder( + CaseExpression $case, + string $expectedSql, + array $expectedParams, + string|int $expectedResult, + ): void { + parent::testCaseExpressionBuilder($case, $expectedSql, $expectedParams, $expectedResult); + } } diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php index 0d1225448..a70f590f1 100644 --- a/tests/Support/TestTrait.php +++ b/tests/Support/TestTrait.php @@ -60,7 +60,7 @@ protected function getDsn(): string return $this->dsn; } - protected function getDriverName(): string + protected static function getDriverName(): string { return 'pgsql'; }