diff --git a/CHANGELOG.md b/CHANGELOG.md index b1923affd..b8c7c3ed7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/Builder/ArrayExpressionBuilder.php b/src/Builder/ArrayExpressionBuilder.php index 7aab3d110..a05590f79 100644 --- a/src/Builder/ArrayExpressionBuilder.php +++ b/src/Builder/ArrayExpressionBuilder.php @@ -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; @@ -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; @@ -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; @@ -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, + }; } } @@ -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), '[]'); @@ -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 ''; diff --git a/src/Builder/ArrayOverlapsBuilder.php b/src/Builder/ArrayOverlapsBuilder.php index 6418e6218..4c9b309c4 100644 --- a/src/Builder/ArrayOverlapsBuilder.php +++ b/src/Builder/ArrayOverlapsBuilder.php @@ -4,10 +4,6 @@ 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; @@ -15,6 +11,8 @@ use Yiisoft\Db\QueryBuilder\Condition\ArrayOverlaps; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; +use function preg_match; + /** * Builds expressions for {@see ArrayOverlaps} for PostgreSQL Server. * @@ -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 { @@ -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[]"; } } diff --git a/src/Builder/JsonOverlapsBuilder.php b/src/Builder/JsonOverlapsBuilder.php index e1a0cdaec..439e4cef3 100644 --- a/src/Builder/JsonOverlapsBuilder.php +++ b/src/Builder/JsonOverlapsBuilder.php @@ -4,10 +4,6 @@ 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; @@ -15,6 +11,8 @@ use Yiisoft\Db\QueryBuilder\Condition\JsonOverlaps; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; +use function preg_match; + /** * Builds expressions for {@see JsonOverlaps} for PostgreSQL Server. * @@ -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 { @@ -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[]"; } } diff --git a/tests/ArrayExpressionBuilderTest.php b/tests/ArrayExpressionBuilderTest.php index f53c9dc2b..79c1493ad 100644 --- a/tests/ArrayExpressionBuilderTest.php +++ b/tests/ArrayExpressionBuilderTest.php @@ -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', @@ -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, @@ -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], @@ -135,7 +158,6 @@ public static function buildProvider(): array [[1, null], null], 'int[][]', 'ARRAY[ARRAY[1,NULL]::int[],NULL]::int[][]', - [], ], ]; } @@ -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(); diff --git a/tests/JsonExpressionBuilderTest.php b/tests/JsonExpressionBuilderTest.php index d08cd37ea..98be1918a 100644 --- a/tests/JsonExpressionBuilderTest.php +++ b/tests/JsonExpressionBuilderTest.php @@ -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); } diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 58366b128..71faac112 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -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[]', []], ]; } @@ -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), @@ -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; } @@ -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, diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index b601d6b0d..1b41bbee7 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -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(); @@ -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); @@ -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);