Skip to content

Implement ArrayMergeBuilder #439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 21, 2025
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
<issueHandlers>
<MixedAssignment errorLevel="suppress" />
<RiskyTruthyFalsyComparison errorLevel="suppress" />
<MoreSpecificImplementedParamType errorLevel="suppress" />
</issueHandlers>
</psalm>
61 changes: 61 additions & 0 deletions src/Builder/ArrayMergeBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace Yiisoft\Db\Pgsql\Builder;

use Yiisoft\Db\Expression\Function\ArrayMerge;
use Yiisoft\Db\Expression\Function\Builder\MultiOperandFunctionBuilder;
use Yiisoft\Db\Expression\Function\MultiOperandFunction;
use Yiisoft\Db\Schema\Column\AbstractArrayColumn;
use Yiisoft\Db\Schema\Column\ColumnInterface;

use function implode;
use function is_string;

/**
* Builds SQL expressions which merge arrays for {@see ArrayMerge} objects.
*
* ```sql
* ARRAY(SELECT DISTINCT UNNEST(operand1::int[] || operand2::int[]))::int[]
* ```
*
* @extends MultiOperandFunctionBuilder<ArrayMerge>
*/
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 . '[]';
}
}
3 changes: 3 additions & 0 deletions src/DQLQueryBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,6 +40,7 @@ protected function defaultExpressionBuilders(): array
Like::class => LikeBuilder::class,
NotLike::class => LikeBuilder::class,
CaseExpression::class => CaseExpressionBuilder::class,
ArrayMerge::class => ArrayMergeBuilder::class,
];
}
}
63 changes: 63 additions & 0 deletions tests/Provider/QueryBuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
],
],
];
}
}
70 changes: 70 additions & 0 deletions tests/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}