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 @@ -53,6 +53,7 @@
- New #440: Add `Connection::getColumnBuilderClass()` method (@Tigrov)
- New #439: Implement `ArrayMergeBuilder` class (@Tigrov)
- Enh #442: Refactor `DMLQueryBuilder::upsert()` method (@Tigrov)
- Enh #444: Improve `ArrayExpressionBuilder` and `JsonExpressionBuilder` classes (@Tigrov)

## 1.3.0 March 21, 2024

Expand Down
24 changes: 18 additions & 6 deletions src/Builder/ArrayExpressionBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Yiisoft\Db\Constant\ColumnType;
use Yiisoft\Db\Constant\DataType;
use Yiisoft\Db\Constant\GettypeResult;
use Yiisoft\Db\Expression\ArrayExpression;
use Yiisoft\Db\Expression\Builder\AbstractArrayExpressionBuilder;
use Yiisoft\Db\Expression\ExpressionInterface;
Expand All @@ -17,6 +18,7 @@
use Yiisoft\Db\Schema\Data\LazyArrayInterface;

use function array_map;
use function gettype;
use function implode;
use function is_array;
use function iterator_to_array;
Expand Down Expand Up @@ -67,21 +69,21 @@ protected function getLazyArrayValue(LazyArrayInterface $value): array|string
/**
* @param string[] $placeholders
*/
private function buildNestedArray(array $placeholders, string $dbType, int $dimension): string
private function buildNestedArray(array $placeholders, string|null $dbType, int $dimension): string
{
$typeHint = $this->getTypeHint($dbType, $dimension);

return 'ARRAY[' . implode(',', $placeholders) . ']' . $typeHint;
}

private function buildNestedSubquery(QueryInterface $query, string $dbType, int $dimension, array &$params): string
private function buildNestedSubquery(QueryInterface $query, string|null $dbType, int $dimension, array &$params): string
{
[$sql, $params] = $this->queryBuilder->build($query, $params);

return "ARRAY($sql)" . $this->getTypeHint($dbType, $dimension);
}

private function buildNestedValue(iterable $value, string $dbType, ColumnInterface|null $column, int $dimension, array &$params): string
private function buildNestedValue(iterable $value, string|null &$dbType, ColumnInterface|null $column, int $dimension, array &$params): string
{
$placeholders = [];
$queryBuilder = $this->queryBuilder;
Expand All @@ -107,6 +109,16 @@ private function buildNestedValue(iterable $value, string $dbType, ColumnInterfa

foreach ($value as $item) {
$placeholders[] = $queryBuilder->buildValue($item, $params);

$dbType ??= match (gettype($item)) {
GettypeResult::ARRAY => 'jsonb',
GettypeResult::BOOLEAN => 'bool',
GettypeResult::INTEGER => 'int',
GettypeResult::RESOURCE => 'bytea',
GettypeResult::STRING => 'text',
GettypeResult::DOUBLE => '',
default => null,
};
}
}

Expand Down Expand Up @@ -145,10 +157,10 @@ private function getColumn(ArrayExpression $expression): AbstractArrayColumn|nul
->fromType(ColumnType::ARRAY, $info);
}

private function getColumnDbType(AbstractArrayColumn|null $column): string
private function getColumnDbType(AbstractArrayColumn|null $column): string|null
{
if ($column === null) {
return '';
return null;
}

return rtrim($this->queryBuilder->getColumnDefinitionBuilder()->buildType($column), '[]');
Expand All @@ -157,7 +169,7 @@ private function getColumnDbType(AbstractArrayColumn|null $column): string
/**
* Return the type hint expression based on type and dimension.
*/
private function getTypeHint(string $dbType, int $dimension): string
private function getTypeHint(string|null $dbType, int $dimension): string
{
if (empty($dbType)) {
return '';
Expand Down
23 changes: 11 additions & 12 deletions src/Builder/ArrayOverlapsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@

namespace Yiisoft\Db\Pgsql\Builder;

use Yiisoft\Db\Exception\Exception;
use InvalidArgumentException;
use Yiisoft\Db\Exception\InvalidConfigException;
use Yiisoft\Db\Exception\NotSupportedException;
use Yiisoft\Db\Expression\ArrayExpression;
use Yiisoft\Db\Expression\Builder\ExpressionBuilderInterface;
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Expression\JsonExpression;
use Yiisoft\Db\QueryBuilder\Condition\ArrayOverlaps;
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;

use function preg_match;

/**
* Builds expressions for {@see ArrayOverlaps} for PostgreSQL Server.
*
Expand All @@ -31,11 +29,6 @@ public function __construct(
* Build SQL for {@see ArrayOverlaps}.
*
* @param ArrayOverlaps $expression The {@see ArrayOverlaps} to be built.
*
* @throws Exception
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws NotSupportedException
*/
public function build(ExpressionInterface $expression, array &$params = []): string
{
Expand All @@ -44,15 +37,21 @@ public function build(ExpressionInterface $expression, array &$params = []): str
: $this->queryBuilder->getQuoter()->quoteColumnName($expression->column);
$values = $expression->values;

if ($values instanceof JsonExpression) {
if (!$values instanceof ExpressionInterface) {
$values = new ArrayExpression($values);
} elseif ($values instanceof JsonExpression) {
/** @psalm-suppress MixedArgument */
$values = new ArrayExpression($values->getValue());
} elseif (!$values instanceof ExpressionInterface) {
$values = new ArrayExpression($values);
}

$values = $this->queryBuilder->buildExpression($values, $params);

if (preg_match('/::\w+\[]$/', $values, $matches) === 1) {
$typeHint = $matches[0];

return "$column$typeHint && $values";
}

return "$column::text[] && $values::text[]";
}
}
17 changes: 8 additions & 9 deletions src/Builder/JsonOverlapsBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,15 @@

namespace Yiisoft\Db\Pgsql\Builder;

use Yiisoft\Db\Exception\Exception;
use InvalidArgumentException;
use Yiisoft\Db\Exception\InvalidConfigException;
use Yiisoft\Db\Exception\NotSupportedException;
use Yiisoft\Db\Expression\ArrayExpression;
use Yiisoft\Db\Expression\Builder\ExpressionBuilderInterface;
use Yiisoft\Db\Expression\ExpressionInterface;
use Yiisoft\Db\Expression\JsonExpression;
use Yiisoft\Db\QueryBuilder\Condition\JsonOverlaps;
use Yiisoft\Db\QueryBuilder\QueryBuilderInterface;

use function preg_match;

/**
* Builds expressions for {@see JsonOverlaps} for PostgreSQL Server.
*
Expand All @@ -31,11 +29,6 @@ public function __construct(
* Build SQL for {@see JsonOverlaps}.
*
* @param JsonOverlaps $expression The {@see JsonOverlaps} to be built.
*
* @throws Exception
* @throws InvalidArgumentException
* @throws InvalidConfigException
* @throws NotSupportedException
*/
public function build(ExpressionInterface $expression, array &$params = []): string
{
Expand All @@ -53,6 +46,12 @@ public function build(ExpressionInterface $expression, array &$params = []): str

$values = $this->queryBuilder->buildExpression($values, $params);

if (preg_match('/::\w+\[]$/', $values, $matches) === 1) {
$typeHint = $matches[0];

return "ARRAY(SELECT jsonb_array_elements_text($column::jsonb))$typeHint && $values";
}

return "ARRAY(SELECT jsonb_array_elements_text($column::jsonb)) && $values::text[]";
}
}
42 changes: 32 additions & 10 deletions tests/ArrayExpressionBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,36 @@ public static function buildProvider(): array
return [
'null' => [null, null, 'NULL', []],
'empty' => [[], null, 'ARRAY[]', []],
'list' => [[1, 2, 3], null, 'ARRAY[1,2,3]', []],
'array w/o type' => [
[[true], [1], ['a'], null],
null,
'ARRAY[ARRAY[TRUE]::bool[],ARRAY[1]::int[],ARRAY[:qp0]::text[],NULL]::jsonb[]',
[
':qp0' => new Param('a', DataType::STRING),
],
],
'bool w/o type' => [[true, false, null], null, 'ARRAY[TRUE,FALSE,NULL]::bool[]'],
'int w/o type' => [[1, 2, 3], null, 'ARRAY[1,2,3]::int[]'],
'float w/o type' => [[1.2, 2.0, null], null, 'ARRAY[1.2,2,NULL]'],
'string w/o type' => [
['a', 'b', 'c'],
null,
'ARRAY[:qp0,:qp1,:qp2]::text[]',
[
':qp0' => new Param('a', DataType::STRING),
':qp1' => new Param('b', DataType::STRING),
':qp2' => new Param('c', DataType::STRING),
],
],
'resource w/o type' => [
[$resource = fopen('php://memory', 'rb'), null],
null,
'ARRAY[:qp0,NULL]::bytea[]',
[
':qp0' => new Param($resource, DataType::LOB),
],
],
'ArrayIterator w/o type' => [new ArrayIterator([1, 2, 3]), null, 'ARRAY[1,2,3]::int[]'],
'ArrayIterator' => [
new ArrayIterator(['a', 'b', 'c']),
'varchar',
Expand All @@ -58,21 +87,18 @@ public static function buildProvider(): array
new \Yiisoft\Db\Schema\Data\LazyArray('[1,2,3]'),
ColumnBuilder::integer(),
'ARRAY[1,2,3]::integer[]',
[],
],
'StructuredLazyArray' => [
new StructuredLazyArray('(1,2,3)'),
'int',
'ARRAY[1,2,3]::int[]',
[],
],
'JsonLazyArray' => [
new JsonLazyArray('[1,2,3]'),
ColumnBuilder::array(ColumnBuilder::integer()),
'ARRAY[1,2,3]::integer[]',
[],
],
'Expression' => [[new Expression('now()')], null, 'ARRAY[now()]', []],
'Expression' => [[new Expression('now()')], null, 'ARRAY[now()]'],
'JsonExpression w/o type' => [
[new JsonExpression(['a' => null, 'b' => 123, 'c' => [4, 5]]), new JsonExpression([true])],
null,
Expand Down Expand Up @@ -105,19 +131,16 @@ public static function buildProvider(): array
(new Query(self::getDb()))->select('id')->from('users')->where(['active' => 1]),
null,
'ARRAY(SELECT "id" FROM "users" WHERE "active" = 1)',
[],
],
'Query' => [
[(new Query(self::getDb()))->select('id')->from('users')->where(['active' => 1])],
'integer[][]',
'ARRAY[ARRAY(SELECT "id" FROM "users" WHERE "active" = 1)::integer[]]::integer[][]',
[],
],
'bool' => [
[[[true], [false, null]], [['t', 'f'], null], null],
'bool[][][]',
'ARRAY[ARRAY[ARRAY[TRUE]::bool[],ARRAY[FALSE,NULL]::bool[]]::bool[][],ARRAY[ARRAY[TRUE,TRUE]::bool[],NULL]::bool[][],NULL]::bool[][][]',
[],
],
'associative' => [
['a' => '1', 'b' => null],
Expand All @@ -135,7 +158,6 @@ public static function buildProvider(): array
[[1, null], null],
'int[][]',
'ARRAY[ARRAY[1,NULL]::int[],NULL]::int[][]',
[],
],
];
}
Expand All @@ -145,7 +167,7 @@ public function testBuild(
iterable|LazyArrayInterface|Query|string|null $value,
ColumnInterface|string|null $type,
string $expected,
array $expectedParams
array $expectedParams = [],
): void {
$db = $this->getConnection();
$qb = $db->getQueryBuilder();
Expand Down
4 changes: 2 additions & 2 deletions tests/JsonExpressionBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,13 @@ public function testBuildArrayExpression(): void
$builder = new JsonExpressionBuilder($qb);
$expression = new JsonExpression(new ArrayExpression([1,2,3]));

$this->assertSame('array_to_json(ARRAY[1,2,3])', $builder->build($expression, $params));
$this->assertSame('array_to_json(ARRAY[1,2,3]::int[])', $builder->build($expression, $params));
$this->assertSame([], $params);

$params = [];
$expression = new JsonExpression(new ArrayExpression([1,2,3]), 'jsonb');

$this->assertSame('array_to_json(ARRAY[1,2,3])::jsonb', $builder->build($expression, $params));
$this->assertSame('array_to_json(ARRAY[1,2,3]::int[])::jsonb', $builder->build($expression, $params));
$this->assertSame([], $params);
}

Expand Down
28 changes: 14 additions & 14 deletions tests/Provider/QueryBuilderProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,15 @@ public static function buildCondition(): array
],

/* Checks to verity that operators work correctly */
[['@>', 'id', new ArrayExpression([1])], '"id" @> ARRAY[1]', []],
[['<@', 'id', new ArrayExpression([1])], '"id" <@ ARRAY[1]', []],
[['=', 'id', new ArrayExpression([1])], '"id" = ARRAY[1]', []],
[['<>', 'id', new ArrayExpression([1])], '"id" <> ARRAY[1]', []],
[['>', 'id', new ArrayExpression([1])], '"id" > ARRAY[1]', []],
[['<', 'id', new ArrayExpression([1])], '"id" < ARRAY[1]', []],
[['>=', 'id', new ArrayExpression([1])], '"id" >= ARRAY[1]', []],
[['<=', 'id', new ArrayExpression([1])], '"id" <= ARRAY[1]', []],
[['&&', 'id', new ArrayExpression([1])], '"id" && ARRAY[1]', []],
[['@>', 'id', new ArrayExpression([1])], '"id" @> ARRAY[1]::int[]', []],
[['<@', 'id', new ArrayExpression([1])], '"id" <@ ARRAY[1]::int[]', []],
[['=', 'id', new ArrayExpression([1])], '"id" = ARRAY[1]::int[]', []],
[['<>', 'id', new ArrayExpression([1])], '"id" <> ARRAY[1]::int[]', []],
[['>', 'id', new ArrayExpression([1])], '"id" > ARRAY[1]::int[]', []],
[['<', 'id', new ArrayExpression([1])], '"id" < ARRAY[1]::int[]', []],
[['>=', 'id', new ArrayExpression([1])], '"id" >= ARRAY[1]::int[]', []],
[['<=', 'id', new ArrayExpression([1])], '"id" <= ARRAY[1]::int[]', []],
[['&&', 'id', new ArrayExpression([1])], '"id" && ARRAY[1]::int[]', []],
];
}

Expand Down Expand Up @@ -480,13 +480,13 @@ public static function buildValue(): array
{
$values = parent::buildValue();

$values['array'][1] = 'ARRAY[:qp0,:qp1,:qp2]';
$values['array'][1] = 'ARRAY[:qp0,:qp1,:qp2]::text[]';
$values['array'][2] = [
':qp0' => new Param('a', DataType::STRING),
':qp1' => new Param('b', DataType::STRING),
':qp2' => new Param('c', DataType::STRING),
];
$values['Iterator'][1] = 'ARRAY[:qp0,:qp1,:qp2]';
$values['Iterator'][1] = 'ARRAY[:qp0,:qp1,:qp2]::text[]';
$values['Iterator'][2] = [
':qp0' => new Param('a', DataType::STRING),
':qp1' => new Param('b', DataType::STRING),
Expand Down Expand Up @@ -514,8 +514,8 @@ public static function prepareValue(): array
$values['paramBinary'][0] = "'\\x737472696e67'::bytea";
$values['paramResource'][0] = "'\\x737472696e67'::bytea";
$values['ResourceStream'][0] = "'\\x737472696e67'::bytea";
$values['array'][0] = "ARRAY['a','b','c']";
$values['Iterator'][0] = "ARRAY['a','b','c']";
$values['array'][0] = "ARRAY['a','b','c']::text[]";
$values['Iterator'][0] = "ARRAY['a','b','c']::text[]";

return $values;
}
Expand Down Expand Up @@ -629,7 +629,7 @@ public static function multiOperandFunctionBuilder(): array
'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])))',
'ARRAY(SELECT DISTINCT UNNEST(ARRAY[1,2,3] || ARRAY[5,6,7]::int[] || :qp0 || (SELECT ARRAY[9,10]::int[])))',
[1, 2, 3, 4, 5, 6, 7, 9, 10],
[
':qp0' => $stringParam,
Expand Down
8 changes: 4 additions & 4 deletions tests/QueryBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -471,14 +471,14 @@ public function testArrayOverlapsBuilder(): void
$params = [];
$sql = $qb->buildExpression(new ArrayOverlaps('column', [1, 2, 3]), $params);

$this->assertSame('"column"::text[] && ARRAY[1,2,3]::text[]', $sql);
$this->assertSame('"column"::int[] && ARRAY[1,2,3]::int[]', $sql);
$this->assertSame([], $params);

// Test column as Expression
$params = [];
$sql = $qb->buildExpression(new ArrayOverlaps(new Expression('column'), [1, 2, 3]), $params);

$this->assertSame('column::text[] && ARRAY[1,2,3]::text[]', $sql);
$this->assertSame('column::int[] && ARRAY[1,2,3]::int[]', $sql);
$this->assertSame([], $params);

$db->close();
Expand All @@ -493,7 +493,7 @@ public function testJsonOverlapsBuilder(): void
$sql = $qb->buildExpression(new JsonOverlaps('column', [1, 2, 3]), $params);

$this->assertSame(
'ARRAY(SELECT jsonb_array_elements_text("column"::jsonb)) && ARRAY[1,2,3]::text[]',
'ARRAY(SELECT jsonb_array_elements_text("column"::jsonb))::int[] && ARRAY[1,2,3]::int[]',
$sql
);
$this->assertSame([], $params);
Expand Down Expand Up @@ -644,7 +644,7 @@ public function testMultiOperandFunctionBuilderWithType(
$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",
"ARRAY(SELECT DISTINCT UNNEST(ARRAY[1,2,3]$typeHint || ARRAY[5,6,7]::int[]$typeHint || :qp0$typeHint || (SELECT ARRAY[9,10]::int[])$typeHint))$typeHint",
$qb->buildExpression($arrayMerge, $params)
);
$this->assertSame([':qp0' => $stringParam], $params);
Expand Down