From cf42ef53b79449dee78ed057901dac4195e37c8b Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 15 Jun 2025 20:51:47 +0700 Subject: [PATCH 1/6] Implement `CaseExpressionBuilder` class --- src/Builder/CaseExpressionBuilder.php | 59 +++++++++++++++++++++++++++ src/DQLQueryBuilder.php | 3 ++ tests/CaseExpressionBuilderTest.php | 55 +++++++++++++++++++++++++ tests/Support/TestTrait.php | 2 +- 4 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/Builder/CaseExpressionBuilder.php create mode 100644 tests/CaseExpressionBuilderTest.php diff --git a/src/Builder/CaseExpressionBuilder.php b/src/Builder/CaseExpressionBuilder.php new file mode 100644 index 000000000..501ce907a --- /dev/null +++ b/src/Builder/CaseExpressionBuilder.php @@ -0,0 +1,59 @@ +getWhen(); + + if (empty($whenClauses)) { + throw new InvalidArgumentException('The CASE expression must have at least one WHEN clause.'); + } + + $sql = 'CASE'; + $queryBuilder = $this->queryBuilder; + + $case = $expression->getCase(); + + if ($case !== null) { + $caseTypeHint = $this->buildTypeHint($expression->getCaseType()); + $sql .= ' ' . $this->buildCondition($case, $params) . $caseTypeHint; + } else { + $caseTypeHint = ''; + } + + foreach ($whenClauses as $when) { + $sql .= ' WHEN ' . $this->buildCondition($when->condition, $params) . $caseTypeHint; + $sql .= ' THEN ' . $queryBuilder->buildValue($when->result, $params); + } + + if ($expression->hasElse()) { + $sql .= ' ELSE ' . $queryBuilder->buildValue($expression->getElse(), $params); + } + + return $sql . ' END'; + } + + protected 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/CaseExpressionBuilderTest.php b/tests/CaseExpressionBuilderTest.php new file mode 100644 index 000000000..70f9b8e6b --- /dev/null +++ b/tests/CaseExpressionBuilderTest.php @@ -0,0 +1,55 @@ + [ + (new CaseExpression())->caseType('int') + ->addWhen(1, 'a'), + 'CASE WHEN 1 THEN :qp0 END', + [ + ':qp0' => new Param('a', DataType::STRING), + ], + ], + 'with case and type hint' => [ + (new CaseExpression('expression', 'int')) + ->addWhen(1, 'a') + ->else('b'), + 'CASE expression::int WHEN 1::int THEN :qp0 ELSE :qp1 END', + [ + ':qp0' => new Param('a', DataType::STRING), + ':qp1' => new Param('b', DataType::STRING), + ], + ], + 'with case and type hint with column' => [ + (new CaseExpression('expression', new IntegerColumn())) + ->addWhen(1, 'a') + ->else('b'), + 'CASE expression::integer WHEN 1::integer THEN :qp0 ELSE :qp1 END', + [ + ':qp0' => new Param('a', DataType::STRING), + ':qp1' => new Param('b', DataType::STRING), + ], + ], + ]; + } +} 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'; } From 4de2cbaf4b11f463aa78d63951f2ed24a00fe293 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Mon, 16 Jun 2025 20:16:30 +0700 Subject: [PATCH 2/6] Update --- src/Builder/CaseExpressionBuilder.php | 6 +++--- tests/CaseExpressionBuilderTest.php | 21 +++++++-------------- 2 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/Builder/CaseExpressionBuilder.php b/src/Builder/CaseExpressionBuilder.php index 501ce907a..f45af7c9e 100644 --- a/src/Builder/CaseExpressionBuilder.php +++ b/src/Builder/CaseExpressionBuilder.php @@ -38,17 +38,17 @@ public function build(ExpressionInterface $expression, array &$params = []): str foreach ($whenClauses as $when) { $sql .= ' WHEN ' . $this->buildCondition($when->condition, $params) . $caseTypeHint; - $sql .= ' THEN ' . $queryBuilder->buildValue($when->result, $params); + $sql .= ' THEN ' . $this->buildResult($when->result, $params); } if ($expression->hasElse()) { - $sql .= ' ELSE ' . $queryBuilder->buildValue($expression->getElse(), $params); + $sql .= ' ELSE ' . $this->buildResult($expression->getElse(), $params); } return $sql . ' END'; } - protected function buildTypeHint(string|ColumnInterface $type): string + private function buildTypeHint(string|ColumnInterface $type): string { if (is_string($type)) { return $type === '' ? '' : "::$type"; diff --git a/tests/CaseExpressionBuilderTest.php b/tests/CaseExpressionBuilderTest.php index 70f9b8e6b..f524144a9 100644 --- a/tests/CaseExpressionBuilderTest.php +++ b/tests/CaseExpressionBuilderTest.php @@ -24,30 +24,23 @@ public static function buildProvider(): array ...parent::buildProvider(), 'without case and type hint' => [ (new CaseExpression())->caseType('int') - ->addWhen(1, 'a'), - 'CASE WHEN 1 THEN :qp0 END', - [ - ':qp0' => new Param('a', DataType::STRING), - ], + ->addWhen(1, "'a'"), + "CASE WHEN 1 THEN 'a' END", ], 'with case and type hint' => [ (new CaseExpression('expression', 'int')) ->addWhen(1, 'a') ->else('b'), - 'CASE expression::int WHEN 1::int THEN :qp0 ELSE :qp1 END', - [ - ':qp0' => new Param('a', DataType::STRING), - ':qp1' => new Param('b', DataType::STRING), - ], + 'CASE expression::int WHEN 1::int THEN a ELSE b END', ], 'with case and type hint with column' => [ (new CaseExpression('expression', new IntegerColumn())) - ->addWhen(1, 'a') - ->else('b'), + ->addWhen(1, $paramA = new Param('a', DataType::STRING)) + ->else($paramB = new Param('b', DataType::STRING)), 'CASE expression::integer WHEN 1::integer THEN :qp0 ELSE :qp1 END', [ - ':qp0' => new Param('a', DataType::STRING), - ':qp1' => new Param('b', DataType::STRING), + ':qp0' => $paramA, + ':qp1' => $paramB, ], ], ]; From 0f66c58de5ee33953a0714aa69049b87c12787d0 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 24 Jun 2025 23:39:16 +0700 Subject: [PATCH 3/6] Improve tests --- src/Builder/CaseExpressionBuilder.php | 15 ++++++-- tests/CaseExpressionBuilderTest.php | 48 ------------------------- tests/Provider/QueryBuilderProvider.php | 35 ++++++++++++++++++ tests/QueryBuilderTest.php | 11 ++++++ 4 files changed, 58 insertions(+), 51 deletions(-) delete mode 100644 tests/CaseExpressionBuilderTest.php diff --git a/src/Builder/CaseExpressionBuilder.php b/src/Builder/CaseExpressionBuilder.php index f45af7c9e..bcdcdf24b 100644 --- a/src/Builder/CaseExpressionBuilder.php +++ b/src/Builder/CaseExpressionBuilder.php @@ -25,19 +25,18 @@ public function build(ExpressionInterface $expression, array &$params = []): str } $sql = 'CASE'; - $queryBuilder = $this->queryBuilder; $case = $expression->getCase(); if ($case !== null) { $caseTypeHint = $this->buildTypeHint($expression->getCaseType()); - $sql .= ' ' . $this->buildCondition($case, $params) . $caseTypeHint; + $sql .= ' ' . $this->buildConditionWithTypeHint($case, $caseTypeHint, $params); } else { $caseTypeHint = ''; } foreach ($whenClauses as $when) { - $sql .= ' WHEN ' . $this->buildCondition($when->condition, $params) . $caseTypeHint; + $sql .= ' WHEN ' . $this->buildConditionWithTypeHint($when->condition, $caseTypeHint, $params); $sql .= ' THEN ' . $this->buildResult($when->result, $params); } @@ -48,6 +47,16 @@ public function build(ExpressionInterface $expression, array &$params = []): str return $sql . ' END'; } + private function buildConditionWithTypeHint( + array|bool|ExpressionInterface|float|int|string|null $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)) { diff --git a/tests/CaseExpressionBuilderTest.php b/tests/CaseExpressionBuilderTest.php deleted file mode 100644 index f524144a9..000000000 --- a/tests/CaseExpressionBuilderTest.php +++ /dev/null @@ -1,48 +0,0 @@ - [ - (new CaseExpression())->caseType('int') - ->addWhen(1, "'a'"), - "CASE WHEN 1 THEN 'a' END", - ], - 'with case and type hint' => [ - (new CaseExpression('expression', 'int')) - ->addWhen(1, 'a') - ->else('b'), - 'CASE expression::int WHEN 1::int THEN a ELSE b END', - ], - 'with case and type hint with column' => [ - (new CaseExpression('expression', new IntegerColumn())) - ->addWhen(1, $paramA = new Param('a', DataType::STRING)) - ->else($paramB = new Param('b', DataType::STRING)), - 'CASE expression::integer WHEN 1::integer THEN :qp0 ELSE :qp1 END', - [ - ':qp0' => $paramA, - ':qp1' => $paramB, - ], - ], - ]; - } -} diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 928efb236..c6486bd48 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,37 @@ public static function prepareValue(): array return $values; } + + public static function caseExpressionBuilder(): array + { + return [ + ...parent::caseExpressionBuilder(), + '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); + } } From e8a9ba98e78687e14dc6bfd1b7b4a8667781e202 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 24 Jun 2025 23:42:48 +0700 Subject: [PATCH 4/6] Add line to CHANGELOG.md [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) 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 From a19c76044ab0dbfd1cb2217b0dd27a104e4507af Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 25 Jun 2025 00:43:09 +0700 Subject: [PATCH 5/6] Fix test --- tests/Provider/QueryBuilderProvider.php | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index c6486bd48..e1fb60b81 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -516,8 +516,31 @@ public static function prepareValue(): array 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 [ - ...parent::caseExpressionBuilder(), + ...$data, 'without case and type hint' => [ (new CaseExpression())->caseType('int') ->addWhen(true, "'a'"), From fb840812b3802d2afc9e8fe2718324b316eb3175 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 25 Jun 2025 15:44:16 +0700 Subject: [PATCH 6/6] Make condition `mixed` type --- src/Builder/CaseExpressionBuilder.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Builder/CaseExpressionBuilder.php b/src/Builder/CaseExpressionBuilder.php index bcdcdf24b..10ac63d18 100644 --- a/src/Builder/CaseExpressionBuilder.php +++ b/src/Builder/CaseExpressionBuilder.php @@ -47,11 +47,8 @@ public function build(ExpressionInterface $expression, array &$params = []): str return $sql . ' END'; } - private function buildConditionWithTypeHint( - array|bool|ExpressionInterface|float|int|string|null $condition, - string $typeHint, - array &$params, - ): string { + private function buildConditionWithTypeHint(mixed $condition, string $typeHint, array &$params): string + { $builtCondition = $this->buildCondition($condition, $params); return $typeHint !== '' ? "($builtCondition)$typeHint" : $builtCondition;