Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
- New #984: Add `createQuery()` and `select()` methods to `ConnectionInterface` (@Tigrov)
- Chg #985: Rename `insertWithReturningPks()` to `insertReturningPks()` in `CommandInterface` and `DMLQueryBuilderInterface` (@Tigrov)
- Enh #992: Add optional type casting to `DataReaderInterface` using columns (@Tigrov)
- New #988: Add `CaseExpression` and `CaseExpressionBuilder` to build `CASE-WHEN-THEN-ELSE` SQL expressions (@Tigrov)
- Enh #991: Improve types in `ConnectionInterface::transaction()` (@kikara)

## 1.3.0 March 21, 2024
Expand Down
4 changes: 4 additions & 0 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
use Rector\Config\RectorConfig;
use Rector\Php74\Rector\Property\RestoreDefaultNullToNullableTypePropertyRector;
use Rector\Php80\Rector\Ternary\GetDebugTypeRector;
use Rector\Php81\Rector\Property\ReadOnlyPropertyRector;
use Rector\Php81\Rector\FuncCall\NullToStrictStringFuncCallArgRector;
Expand All @@ -24,6 +25,9 @@
]);

$rectorConfig->skip([
RestoreDefaultNullToNullableTypePropertyRector::class => [
__DIR__ . '/src/Expression/CaseExpression.php',
],
GetDebugTypeRector::class => [
__DIR__ . '/tests/AbstractColumnTest.php',
],
Expand Down
207 changes: 207 additions & 0 deletions src/Expression/CaseExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Expression;

use Yiisoft\Db\Query\QueryInterface;
use Yiisoft\Db\Schema\Column\ColumnInterface;

use function array_key_exists;
use function get_object_vars;

/**
* Represents a SQL CASE expression.
*
* A CASE expression allows conditional logic in SQL queries, returning different values based on specified conditions.
* It can be used to implement complex logic directly in SQL statements.
*
* Example usage:
*
* ```php
* $case = (new CaseExpression())
* ->addWhen('condition1', 'result1')
* ->addWhen('condition2', 'result2')
* ->else('defaultResult');
* ```
*
* This will be generated into a SQL CASE expression like:
*
* ```sql
* CASE
* WHEN condition1 THEN result1
* WHEN condition2 THEN result2
* ELSE defaultResult
* END
* ```
*
* Example with a specific case value:
*
* ```php
* $case = (new CaseExpression('expression'))
* ->addWhen(1, 'result1')
* ->addWhen(2, 'result2')
* ->else('defaultResult');
* ```
*
* This will be generated into a SQL CASE expression like:
*
* ```sql
* CASE expression
* WHEN 1 THEN result1
* WHEN 2 THEN result2
* ELSE defaultResult
* END
* ```
*/
final class CaseExpression implements ExpressionInterface
{
/**
* @var WhenClause[] List of WHEN conditions and their corresponding results in the CASE expression.
*/
private array $whenClauses;
/**
* @var mixed The result to return if no conditions match in the CASE expression.
* If not set, the CASE expression will not have an ELSE clause.
*
* @psalm-suppress PropertyNotSetInConstructor
*/
private mixed $else;

/**
* @param mixed $case Comparison condition in the CASE expression:
* - `string` is treated as a SQL expression;
* - `array` is treated as a condition to check, see {@see QueryInterface::where()};
* - other values will be converted to their string representation using {@see QueryBuilderInterface::buildValue()}.
* If not provided, the CASE expression will be a WHEN-THEN structure without a specific case value.
* @param ColumnInterface|string $caseType Optional data type of the CASE expression which can be used in some DBMS
* to specify the expected type (for example in PostgreSQL).
* @param WhenClause ...$when List of WHEN conditions and their corresponding results in the CASE expression.
*/
public function __construct(
private mixed $case = null,
private string|ColumnInterface $caseType = '',
WhenClause ...$when,
) {
$this->whenClauses = $when;
}

/**
* Adds a condition and its corresponding result to the CASE expression.
*
* @param mixed $when The condition to check (WHEN):
* - `string` is treated as a SQL expression;
* - `array` is treated as a condition to check, see {@see QueryInterface::where()};
* - other values will be converted to their string representation using {@see QueryBuilderInterface::buildValue()}.
* @param mixed $then The result to return if the condition is `true` (THEN):
* - `string` is treated as a SQL expression;
* - other values will be converted to their string representation using {@see QueryBuilderInterface::buildValue()}.
*/
public function addWhen(mixed $when, mixed $then): self
{
$this->whenClauses[] = new WhenClause($when, $then);
return $this;
}

/**
* Sets the value to compare against in the CASE expression.
*
* @param mixed $case Comparison condition in the CASE expression:
* - `string` is treated as a SQL expression;
* - `array` is treated as a condition to check, see {@see QueryInterface::where()};
* - other values will be converted to their string representation using {@see QueryBuilderInterface::buildValue()}.
* If not provided, the CASE expression will be a WHEN-THEN structure without a specific case value.
*/
public function case(mixed $case): self
{
$this->case = $case;
return $this;
}

/**
* Sets the optional data type of the CASE expression which can be used in some DBMS to specify the expected type
* (for example in PostgreSQL).
*/
public function caseType(string|ColumnInterface $caseType): self
{
$this->caseType = $caseType;
return $this;
}

/**
* Sets the result to return if no conditions match in the CASE expression.
*
* @param mixed $else The result to return if no conditions match (ELSE).
* - `string` is treated as a SQL expression;
* - other values will be converted to their string representation using {@see QueryBuilderInterface::buildValue()}.
* If not set, the CASE expression will not have an ELSE clause.
*/
public function else(mixed $else): self
{
$this->else = $else;
return $this;
}

/**
* Returns the comparison condition in the CASE expression.
*
* @psalm-mutation-free
*/
public function getCase(): mixed
{
return $this->case;
}

/**
* Returns the data type of the CASE expression.
*
* @psalm-mutation-free
*/
public function getCaseType(): string|ColumnInterface
{
return $this->caseType;
}

/**
* Returns the result to return if no conditions match in the CASE expression.
*
* @psalm-mutation-free
*/
public function getElse(): mixed
{
return $this->else ?? null;
}

/**
* Returns WHEN conditions and their corresponding results in the CASE expression.
*
* @return WhenClause[] List of WHEN conditions and their corresponding results in the CASE expression.
*
* @psalm-mutation-free
*/
public function getWhen(): array
{
return $this->whenClauses;
}

/**
* Returns `true` if the CASE expression has an ELSE clause, `false` otherwise.
*
* @psalm-mutation-free
*/
public function hasElse(): bool
{
return array_key_exists('else', get_object_vars($this));
}

/**
* Sets WHEN conditions and their corresponding results in the CASE expression.
*
* @param WhenClause ...$whenClauses List of WHEN conditions and their corresponding results in the CASE expression.
*/
public function setWhen(WhenClause ...$whenClauses): self
{
$this->whenClauses = $whenClauses;
return $this;
}
}
90 changes: 90 additions & 0 deletions src/Expression/CaseExpressionBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Expression;

use InvalidArgumentException;
use Yiisoft\Db\Constant\GettypeResult;
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;

use function gettype;
use function is_string;

/**
* Builds expressions for {@see CaseExpression}.
*/
class CaseExpressionBuilder implements ExpressionBuilderInterface
{
public function __construct(protected readonly QueryBuilderInterface $queryBuilder)
{
}

/**
* Builds an SQL CASE expression from the given {@see CaseExpression} object.
*
* @param CaseExpression $expression The CASE expression to build.
* @param array $params The parameters to be bound to the query.
*
* @return string SQL CASE expression.
*/
public function build(ExpressionInterface $expression, array &$params = []): string
{
$whenClauses = $expression->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) {
$sql .= ' ' . $this->buildCondition($case, $params);
}

foreach ($whenClauses as $when) {
$sql .= ' WHEN ' . $this->buildCondition($when->condition, $params);
$sql .= ' THEN ' . $this->buildResult($when->result, $params);
}

if ($expression->hasElse()) {
$sql .= ' ELSE ' . $this->buildResult($expression->getElse(), $params);
}

return $sql . ' END';
}

/**
* Builds the condition part of the CASE expression based on their type.
*
* @return string The SQL condition string.
*/
protected function buildCondition(mixed $condition, array &$params): string
{
/**
* @var string
* @psalm-suppress MixedArgument
*/
return match (gettype($condition)) {
GettypeResult::ARRAY => $this->queryBuilder->buildCondition($condition, $params),
GettypeResult::STRING => $condition,
default => $this->queryBuilder->buildValue($condition, $params),
};
}

/**
* Builds the result part of the CASE expression based on its type.
*
* @return string The SQL result string.
*/
protected function buildResult(mixed $result, array &$params): string
{
if (is_string($result)) {
return $result;
}

return $this->queryBuilder->buildValue($result, $params);
}
}
31 changes: 31 additions & 0 deletions src/Expression/WhenClause.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Expression;

use Yiisoft\Db\Query\QueryInterface;
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;

/**
* Represents the condition and the result of a WHEN clause in a SQL CASE statement.
*
* @see CaseExpression
*/
final class WhenClause
{
/**
* @param mixed $condition The condition for the WHEN clause:
* - `string` is treated as a SQL expression;
* - `array` is treated as a condition to check, see {@see QueryInterface::where()};
* - other values will be converted to their string representation using {@see QueryBuilderInterface::buildValue()}.
* @param mixed $result The result to return if the condition is `true`:
* - `string` is treated as a SQL expression;
* - other values will be converted to their string representation using {@see QueryBuilderInterface::buildValue()}.
*/
public function __construct(
public readonly mixed $condition,
public readonly mixed $result,
) {
}
}
3 changes: 3 additions & 0 deletions src/QueryBuilder/AbstractDQLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Expression\JsonExpression;
use Yiisoft\Db\Expression\JsonExpressionBuilder;
use Yiisoft\Db\Expression\CaseExpression;
use Yiisoft\Db\Expression\CaseExpressionBuilder;
use Yiisoft\Db\Expression\StructuredExpression;
use Yiisoft\Db\Expression\StructuredExpressionBuilder;
use Yiisoft\Db\QueryBuilder\Condition\HashCondition;
Expand Down Expand Up @@ -548,6 +550,7 @@ protected function defaultExpressionBuilders(): array
JsonExpression::class => JsonExpressionBuilder::class,
ArrayExpression::class => ArrayExpressionBuilder::class,
StructuredExpression::class => StructuredExpressionBuilder::class,
CaseExpression::class => CaseExpressionBuilder::class,
];
}

Expand Down
Loading