diff --git a/CHANGELOG.md b/CHANGELOG.md index ecaef217..99d2ad24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ - Enh #377: Remove `TableSchema` class and refactor `Schema` class (@Tigrov) - Enh #380: Support column's collation (@Tigrov) - New #385: Add `Connection::getColumnBuilderClass()` method (@Tigrov) +- New #384: Implement `ArrayMergeBuilder`, `GreatestBuilder` and `LeastBuilder` classes (@Tigrov) ## 1.2.0 March 21, 2024 diff --git a/psalm.xml b/psalm.xml index 792aaa6f..27907564 100644 --- a/psalm.xml +++ b/psalm.xml @@ -17,5 +17,6 @@ + diff --git a/src/Builder/ArrayMergeBuilder.php b/src/Builder/ArrayMergeBuilder.php new file mode 100644 index 00000000..be0f2f62 --- /dev/null +++ b/src/Builder/ArrayMergeBuilder.php @@ -0,0 +1,48 @@ + + */ +final class ArrayMergeBuilder extends MultiOperandFunctionBuilder +{ + /** + * Builds a SQL expression which merges arrays from the given {@see ArrayMerge} object. + * + * @param ArrayMerge $expression The expression to build. + * @param array $params The parameters to bind. + * + * @return string The SQL expression. + */ + protected function buildFromExpression(MultiOperandFunction $expression, array &$params): string + { + $selects = []; + + foreach ($expression->getOperands() as $operand) { + $builtOperand = $this->buildOperand($operand, $params); + + $selects[] = "SELECT value FROM json_each($builtOperand)"; + } + + return '(SELECT json_group_array(value) AS value FROM (' . implode(' UNION ', $selects) . '))'; + } +} diff --git a/src/Builder/GreatestBuilder.php b/src/Builder/GreatestBuilder.php new file mode 100644 index 00000000..49ac8bb4 --- /dev/null +++ b/src/Builder/GreatestBuilder.php @@ -0,0 +1,38 @@ + + */ +final class GreatestBuilder extends MultiOperandFunctionBuilder +{ + /** + * Builds a SQL `MAX()` function expression from the given {@see Greatest} object. + * + * @param Greatest $expression The expression to build. + * @param array $params The parameters to bind. + * + * @return string The SQL `MAX()` function expression. + */ + protected function buildFromExpression(MultiOperandFunction $expression, array &$params): string + { + $builtOperands = []; + + foreach ($expression->getOperands() as $operand) { + $builtOperands[] = $this->buildOperand($operand, $params); + } + + return 'MAX(' . implode(', ', $builtOperands) . ')'; + } +} diff --git a/src/Builder/LeastBuilder.php b/src/Builder/LeastBuilder.php new file mode 100644 index 00000000..0e208f34 --- /dev/null +++ b/src/Builder/LeastBuilder.php @@ -0,0 +1,38 @@ + + */ +final class LeastBuilder extends MultiOperandFunctionBuilder +{ + /** + * Builds a SQL `MIN()` function expression from the given {@see Least} object. + * + * @param Least $expression The expression to build. + * @param array $params The parameters to bind. + * + * @return string The SQL `MIN()` function expression. + */ + protected function buildFromExpression(MultiOperandFunction $expression, array &$params): string + { + $builtOperands = []; + + foreach ($expression->getOperands() as $operand) { + $builtOperands[] = $this->buildOperand($operand, $params); + } + + return 'MIN(' . implode(', ', $builtOperands) . ')'; + } +} diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index a1b5c4f2..f20b5d78 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -6,6 +6,9 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\ExpressionInterface; +use Yiisoft\Db\Expression\Function\ArrayMerge; +use Yiisoft\Db\Expression\Function\Greatest; +use Yiisoft\Db\Expression\Function\Least; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder; @@ -14,8 +17,11 @@ use Yiisoft\Db\QueryBuilder\Condition\JsonOverlaps; use Yiisoft\Db\QueryBuilder\Condition\NotIn; use Yiisoft\Db\QueryBuilder\Condition\NotLike; +use Yiisoft\Db\Sqlite\Builder\ArrayMergeBuilder; +use Yiisoft\Db\Sqlite\Builder\GreatestBuilder; use Yiisoft\Db\Sqlite\Builder\InBuilder; use Yiisoft\Db\Sqlite\Builder\JsonOverlapsBuilder; +use Yiisoft\Db\Sqlite\Builder\LeastBuilder; use Yiisoft\Db\Sqlite\Builder\LikeBuilder; use function array_filter; @@ -142,6 +148,9 @@ protected function defaultExpressionBuilders(): array NotLike::class => LikeBuilder::class, In::class => InBuilder::class, NotIn::class => InBuilder::class, + ArrayMerge::class => ArrayMergeBuilder::class, + Greatest::class => GreatestBuilder::class, + Least::class => LeastBuilder::class, ]; } } diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 5828ccf4..65198551 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -7,6 +7,7 @@ use Yiisoft\Db\Constant\DataType; use Yiisoft\Db\Constant\PseudoType; use Yiisoft\Db\Expression\Expression; +use Yiisoft\Db\Expression\Function\ArrayMerge; use Yiisoft\Db\Expression\Param; use Yiisoft\Db\Query\Query; use Yiisoft\Db\QueryBuilder\Condition\In; @@ -16,6 +17,7 @@ use function array_replace; use function array_splice; +use function strtr; final class QueryBuilderProvider extends \Yiisoft\Db\Tests\Provider\QueryBuilderProvider { @@ -413,4 +415,55 @@ public static function prepareValue(): array return $values; } + + public static function multiOperandFunctionClasses(): array + { + return [ + ...parent::multiOperandFunctionClasses(), + ArrayMerge::class => [ArrayMerge::class], + ]; + } + + public static function multiOperandFunctionBuilder(): array + { + $data = parent::multiOperandFunctionBuilder(); + + $intQuery = self::getDb()->select(10); + $intQuerySql = '(SELECT 10)'; + $stringParam = new Param('[3,4,5]', DataType::STRING); + + foreach ($data as &$value) { + $value[2] = strtr($value[2], [ + 'GREATEST(' => 'MAX(', + 'LEAST(' => 'MIN(', + ]); + } + + return [ + ...$data, + 'ArrayMerge with 1 operand' => [ + ArrayMerge::class, + ["'[1,2,3]'"], + "('[1,2,3]')", + [1, 2, 3], + ], + 'ArrayMerge with 2 operands' => [ + ArrayMerge::class, + ["'[1,2,3]'", $stringParam], + "(SELECT json_group_array(value) AS value FROM (SELECT value FROM json_each('[1,2,3]') UNION SELECT value FROM json_each(:qp0)))", + [1, 2, 3, 4, 5], + [':qp0' => $stringParam], + ], + 'ArrayMerge with 4 operands' => [ + ArrayMerge::class, + ["'[1,2,3]'", [5,6,7], $stringParam, $intQuery], + "(SELECT json_group_array(value) AS value FROM (SELECT value FROM json_each('[1,2,3]') UNION SELECT value FROM json_each(:qp0) UNION SELECT value FROM json_each(:qp1) UNION SELECT value FROM json_each($intQuerySql)))", + [1, 2, 3, 4, 5, 6, 7, 10], + [ + ':qp0' => new Param('[5,6,7]', DataType::STRING), + ':qp1' => $stringParam, + ], + ], + ]; + } } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 985a2a39..0aa14801 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -719,4 +719,31 @@ public function testCaseExpressionBuilder( ): void { parent::testCaseExpressionBuilder($case, $expectedSql, $expectedParams, $expectedResult); } + + #[DataProviderExternal(QueryBuilderProvider::class, 'lengthBuilder')] + public function testLengthBuilder( + string|ExpressionInterface $operand, + string $expectedSql, + int $expectedResult, + array $expectedParams = [], + ): void { + parent::testLengthBuilder($operand, $expectedSql, $expectedResult, $expectedParams); + } + + #[DataProviderExternal(QueryBuilderProvider::class, 'multiOperandFunctionBuilder')] + public function testMultiOperandFunctionBuilder( + string $class, + array $operands, + string $expectedSql, + array|string|int $expectedResult, + array $expectedParams = [], + ): void { + parent::testMultiOperandFunctionBuilder($class, $operands, $expectedSql, $expectedResult, $expectedParams); + } + + #[DataProviderExternal(QueryBuilderProvider::class, 'multiOperandFunctionClasses')] + public function testMultiOperandFunctionBuilderWithoutOperands(string $class): void + { + parent::testMultiOperandFunctionBuilderWithoutOperands($class); + } }