diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ac0a0dd3..4631dece9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ - Enh #431: Remove `TableSchema` class and refactor `Schema` class (@Tigrov) - Enh #433: Support column's collation (@Tigrov) - New #440: Add `Connection::getColumnBuilderClass()` method (@Tigrov) +- New #439: Implement `ArrayMergeBuilder` class (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/psalm.xml b/psalm.xml index 8bb4c4cc3..c5ebe2e43 100644 --- a/psalm.xml +++ b/psalm.xml @@ -18,5 +18,6 @@ + diff --git a/src/Builder/ArrayMergeBuilder.php b/src/Builder/ArrayMergeBuilder.php new file mode 100644 index 000000000..1dbbe7f3d --- /dev/null +++ b/src/Builder/ArrayMergeBuilder.php @@ -0,0 +1,61 @@ + + */ +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 + { + $typeHint = $this->buildTypeHint($expression->getType()); + $builtOperands = []; + + foreach ($expression->getOperands() as $operand) { + $builtOperands[] = $this->buildOperand($operand, $params) . $typeHint; + } + + return 'ARRAY(SELECT DISTINCT UNNEST(' . implode(' || ', $builtOperands) . "))$typeHint"; + } + + private function buildTypeHint(string|ColumnInterface $type): string + { + if (is_string($type)) { + return $type === '' ? '' : "::$type"; + } + + $typeHint = '::' . $this->queryBuilder->getColumnDefinitionBuilder()->buildType($type); + + if ($type instanceof AbstractArrayColumn) { + return $typeHint; + } + + return $typeHint . '[]'; + } +} diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index d53b553c2..885b18d4e 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -6,9 +6,11 @@ use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\CaseExpression; +use Yiisoft\Db\Expression\Function\ArrayMerge; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Expression\StructuredExpression; use Yiisoft\Db\Pgsql\Builder\ArrayExpressionBuilder; +use Yiisoft\Db\Pgsql\Builder\ArrayMergeBuilder; use Yiisoft\Db\Pgsql\Builder\ArrayOverlapsBuilder; use Yiisoft\Db\Pgsql\Builder\CaseExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\JsonOverlapsBuilder; @@ -38,6 +40,7 @@ protected function defaultExpressionBuilders(): array Like::class => LikeBuilder::class, NotLike::class => LikeBuilder::class, CaseExpression::class => CaseExpressionBuilder::class, + ArrayMerge::class => ArrayMergeBuilder::class, ]; } } diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index f2508f59e..4aa1367e2 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -9,6 +9,7 @@ use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\CaseExpression; use Yiisoft\Db\Expression\Expression; +use Yiisoft\Db\Expression\Function\ArrayMerge; use Yiisoft\Db\Expression\Param; use Yiisoft\Db\Pgsql\Column\ColumnBuilder; use Yiisoft\Db\Pgsql\Column\IntegerColumn; @@ -574,4 +575,66 @@ public static function caseExpressionBuilder(): array ], ]; } + + public static function multiOperandFunctionClasses(): array + { + return [ + ...parent::multiOperandFunctionClasses(), + ArrayMerge::class => [ArrayMerge::class], + ]; + } + + public static function lengthBuilder(): array + { + return [ + ...parent::lengthBuilder(), + 'query' => [ + self::getDb()->select(new Expression("'four'::text")), + self::replaceQuotes("LENGTH((SELECT 'four'::text))"), + 4, + ], + ]; + } + + public static function multiOperandFunctionBuilder(): array + { + $data = parent::multiOperandFunctionBuilder(); + + $stringQuery = self::getDb()->select(new Expression("'longest'::text")); + $stringQuerySql = "(SELECT 'longest'::text)"; + $stringParam = new Param('{3,4,5}', DataType::STRING); + + $data['Longest with 3 operands'][1][1] = $stringQuery; + $data['Longest with 3 operands'][2] = "(SELECT value FROM (SELECT 'short' AS value UNION SELECT $stringQuerySql" + . ' AS value UNION SELECT :qp0 AS value) AS t ORDER BY LENGTH(value) DESC LIMIT 1)'; + $data['Shortest with 3 operands'][1][1] = $stringQuery; + $data['Shortest with 3 operands'][2] = "(SELECT value FROM (SELECT 'short' AS value UNION SELECT $stringQuerySql" + . ' AS value UNION SELECT :qp0 AS value) AS t ORDER BY LENGTH(value) ASC LIMIT 1)'; + + return [ + ...$data, + 'ArrayMerge with 1 operand' => [ + ArrayMerge::class, + ['ARRAY[1,2,3]'], + '(ARRAY[1,2,3])', + [1, 2, 3], + ], + 'ArrayMerge with 2 operands' => [ + ArrayMerge::class, + ['ARRAY[1,2,3]', $stringParam], + 'ARRAY(SELECT DISTINCT UNNEST(ARRAY[1,2,3] || :qp0))', + [1, 2, 3, 4, 5], + [':qp0' => $stringParam], + ], + 'ArrayMerge with 4 operands' => [ + ArrayMerge::class, + ['ARRAY[1,2,3]', [5, 6, 7], $stringParam, self::getDb()->select(new ArrayExpression([9, 10]))], + 'ARRAY(SELECT DISTINCT UNNEST(ARRAY[1,2,3] || ARRAY[5,6,7] || :qp0 || (SELECT ARRAY[9,10])))', + [1, 2, 3, 4, 5, 6, 7, 9, 10], + [ + ':qp0' => $stringParam, + ], + ], + ]; + } } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 094a74beb..782fe5b16 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -5,12 +5,19 @@ namespace Yiisoft\Db\Pgsql\Tests; use PHPUnit\Framework\Attributes\DataProviderExternal; +use PHPUnit\Framework\Attributes\TestWith; +use Yiisoft\Db\Constant\DataType; use Yiisoft\Db\Driver\Pdo\PdoConnectionInterface; use Yiisoft\Db\Exception\IntegrityException; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Expression\ArrayExpression; use Yiisoft\Db\Expression\CaseExpression; use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\ExpressionInterface; +use Yiisoft\Db\Expression\Function\ArrayMerge; +use Yiisoft\Db\Expression\Param; +use Yiisoft\Db\Pgsql\Column\ArrayColumn; +use Yiisoft\Db\Pgsql\Column\IntegerColumn; use Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; use Yiisoft\Db\Query\Query; @@ -587,4 +594,67 @@ 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); + } + + #[TestWith(['int[]', '::int[]', '{1,2,3,4,5,6,7,9,10}'])] + #[TestWith([new IntegerColumn(), '::integer[]', '{1,2,3,4,5,6,7,9,10}'])] + #[TestWith([new ArrayColumn(), '::varchar[]', '{1,2,3,4,5,6,7,9,10}'])] + #[TestWith([new ArrayColumn(column: new IntegerColumn()), '::integer[]', '{1,2,3,4,5,6,7,9,10}'])] + public function testMultiOperandFunctionBuilderWithType( + string|ColumnInterface $type, + string $typeHint, + string $expectedResult, + ): void { + $db = $this->getConnection(); + $qb = $db->getQueryBuilder(); + + $stringParam = new Param('{3,4,5}', DataType::STRING); + $arrayMerge = (new ArrayMerge( + 'ARRAY[1,2,3]', + [5, 6, 7], + $stringParam, + self::getDb()->select(new ArrayExpression([9, 10])), + ))->type($type); + $params = []; + + $this->assertSame( + "ARRAY(SELECT DISTINCT UNNEST(ARRAY[1,2,3]$typeHint || ARRAY[5,6,7]$typeHint || :qp0$typeHint || (SELECT ARRAY[9,10])$typeHint))$typeHint", + $qb->buildExpression($arrayMerge, $params) + ); + $this->assertSame([':qp0' => $stringParam], $params); + + $arrayCol = new ArrayColumn(column: new IntegerColumn()); + $result = $db->select($arrayMerge)->scalar(); + $result = $arrayCol->phpTypecast($result); + sort($result, SORT_NUMERIC); + $expectedResult = $arrayCol->phpTypecast($expectedResult); + + $this->assertSame($expectedResult, $result); + } }