From d42f5a66ce4c738536081a27fe7eb9773ce8e7f9 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 29 Jun 2024 14:53:02 +0700 Subject: [PATCH 1/5] Add array and json overlaps condition builders --- src/Builder/ArrayOverlapsConditionBuilder.php | 47 ++++++++++ src/Builder/JsonOverlapsConditionBuilder.php | 47 ++++++++++ src/DQLQueryBuilder.php | 6 ++ tests/CommandTest.php | 45 --------- tests/Provider/QueryBuilderProvider.php | 13 +++ tests/QueryBuilderTest.php | 94 +++++++++++++++++++ tests/Support/Fixture/pgsql.sql | 4 + tests/Support/TestTrait.php | 9 ++ 8 files changed, 220 insertions(+), 45 deletions(-) create mode 100644 src/Builder/ArrayOverlapsConditionBuilder.php create mode 100644 src/Builder/JsonOverlapsConditionBuilder.php diff --git a/src/Builder/ArrayOverlapsConditionBuilder.php b/src/Builder/ArrayOverlapsConditionBuilder.php new file mode 100644 index 000000000..41f0a3617 --- /dev/null +++ b/src/Builder/ArrayOverlapsConditionBuilder.php @@ -0,0 +1,47 @@ +prepareColumn($expression->getColumn()); + $values = $expression->getValues(); + + if ($values instanceof JsonExpression) { + $values = new ArrayExpression($values->getValue()); + } elseif (!$values instanceof ExpressionInterface) { + $values = new ArrayExpression($values); + } + + $values = $this->queryBuilder->buildExpression($values, $params); + + return "$column::text[] && $values::text[]"; + } +} diff --git a/src/Builder/JsonOverlapsConditionBuilder.php b/src/Builder/JsonOverlapsConditionBuilder.php new file mode 100644 index 000000000..3f6565a37 --- /dev/null +++ b/src/Builder/JsonOverlapsConditionBuilder.php @@ -0,0 +1,47 @@ +prepareColumn($expression->getColumn()); + $values = $expression->getValues(); + + if ($values instanceof JsonExpression) { + $values = new ArrayExpression($values->getValue()); + } elseif (!$values instanceof ExpressionInterface) { + $values = new ArrayExpression($values); + } + + $values = $this->queryBuilder->buildExpression($values, $params); + + return "ARRAY(SELECT jsonb_array_elements($column::jsonb))::text[] && $values::text[]"; + } +} diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index 040d4556f..20798d71c 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -9,10 +9,14 @@ use Yiisoft\Db\Expression\ExpressionBuilderInterface; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Pgsql\Builder\ArrayExpressionBuilder; +use Yiisoft\Db\Pgsql\Builder\ArrayOverlapsConditionBuilder; +use Yiisoft\Db\Pgsql\Builder\JsonOverlapsConditionBuilder; use Yiisoft\Db\Pgsql\Builder\StructuredExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\ExpressionBuilder; use Yiisoft\Db\Pgsql\Builder\JsonExpressionBuilder; use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder; +use Yiisoft\Db\QueryBuilder\Condition\ArrayOverlapsCondition; +use Yiisoft\Db\QueryBuilder\Condition\JsonOverlapsCondition; use Yiisoft\Db\QueryBuilder\Condition\LikeCondition; use function array_merge; @@ -52,7 +56,9 @@ protected function defaultExpressionBuilders(): array { return array_merge(parent::defaultExpressionBuilders(), [ ArrayExpression::class => ArrayExpressionBuilder::class, + ArrayOverlapsCondition::class => ArrayOverlapsConditionBuilder::class, JsonExpression::class => JsonExpressionBuilder::class, + JsonOverlapsCondition::class => JsonOverlapsConditionBuilder::class, StructuredExpression::class => StructuredExpressionBuilder::class, Expression::class => ExpressionBuilder::class, ]); diff --git a/tests/CommandTest.php b/tests/CommandTest.php index 5b59e25a8..e008cbd43 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -182,51 +182,6 @@ public function testDropDefaultValue(): void $db->close(); } - /** - * @throws Exception - * @throws InvalidConfigException - * @throws Throwable - * - * {@link https://github.com/yiisoft/yii2/issues/15827} - */ - public function testIssue15827(): void - { - $db = $this->getConnection(); - - $command = $db->createCommand(); - $inserted = $command->insert( - '{{array_and_json_types}}', - [ - 'jsonb_col' => new JsonExpression(['Solution date' => '13.01.2011']), - ], - )->execute(); - - $this->assertSame(1, $inserted); - - $found = $command->setSql( - << '{"Some not existing key": "random value"}' - SQL, - )->execute(); - - $this->assertSame(0, $found); - - $found = $command->setSql( - << '{"Solution date": "13.01.2011"}' - SQL, - )->execute(); - - $this->assertSame(1, $found); - $this->assertSame(1, $command->delete('{{array_and_json_types}}')->execute()); - - $db->close(); - } - /** * @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\CommandProvider::rawSql * diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index aa5c5e8cd..3fc9a1e2d 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -523,4 +523,17 @@ public static function upsert(): array return $upsert; } + + public static function overlapsCondition(): array + { + $data = parent::overlapsCondition(); + + $data['null'][1] = 0; + $data['expression'][0] = new Expression("'{0,1,2,7}'"); + $data['query expression'][0] = (new Query(static::getDb()))->select(new ArrayExpression([0,1,2,7])); + $data[] = [new Expression('ARRAY[0,1,2,7]'), 1]; + $data[] = [new ArrayExpression([0,1,2,7]), 1]; + + return $data; + } } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 6f8cd2481..3c87fabaf 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -13,7 +13,10 @@ use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Column; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; +use Yiisoft\Db\Query\Query; use Yiisoft\Db\Query\QueryInterface; +use Yiisoft\Db\QueryBuilder\Condition\ArrayOverlapsCondition; +use Yiisoft\Db\QueryBuilder\Condition\JsonOverlapsCondition; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Tests\Common\CommonQueryBuilderTest; @@ -681,4 +684,95 @@ public function testSelectScalar(array|bool|float|int|string $columns, string $e { parent::testSelectScalar($columns, $expected); } + + public function testArrayOverlapsConditionBuilder(): void + { + $db = $this->getConnection(); + $qb = $db->getQueryBuilder(); + + $params = []; + $sql = $qb->buildExpression(new ArrayOverlapsCondition('column', [1, 2, 3]), $params); + + $this->assertSame('"column"::text[] && ARRAY[:qp0, :qp1, :qp2]::text[]', $sql); + $this->assertSame([':qp0' => 1, ':qp1' => 2, ':qp2' => 3], $params); + + $db->close(); + } + + public function testJsonOverlapsConditionBuilder(): void + { + $db = $this->getConnection(); + $qb = $db->getQueryBuilder(); + + $params = []; + $sql = $qb->buildExpression(new JsonOverlapsCondition('column', [1, 2, 3]), $params); + + $this->assertSame( + 'ARRAY(SELECT jsonb_array_elements("column"::jsonb))::text[] && ARRAY[:qp0, :qp1, :qp2]::text[]', + $sql + ); + $this->assertSame([':qp0' => 1, ':qp1' => 2, ':qp2' => 3], $params); + + $db->close(); + } + + /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::overlapsCondition */ + public function testOverlapsCondition(iterable|ExpressionInterface $values, int $expectedCount): void + { + $db = $this->getConnection(); + $query = new Query($db); + + $count = $query + ->from('array_and_json_types') + ->where(new ArrayOverlapsCondition('intarray_col', $values)) + ->count(); + + $this->assertSame($expectedCount, $count); + + $count = $query + ->from('array_and_json_types') + ->where(new JsonOverlapsCondition('json_col', $values)) + ->count(); + + $this->assertSame($expectedCount, $count); + + $count = $query + ->from('array_and_json_types') + ->where(new JsonOverlapsCondition('jsonb_col', $values)) + ->count(); + + $this->assertSame($expectedCount, $count); + + $db->close(); + } + + /** @dataProvider \Yiisoft\Db\Pgsql\Tests\Provider\QueryBuilderProvider::overlapsCondition */ + public function testOverlapsConditionOperator(iterable|ExpressionInterface $values, int $expectedCount): void + { + $db = $this->getConnection(); + $query = new Query($db); + + $count = $query + ->from('array_and_json_types') + ->where(['array overlaps', 'intarray_col', $values]) + ->count(); + + $this->assertSame($expectedCount, $count); + + $count = $query + ->from('array_and_json_types') + ->where(['json overlaps', 'json_col', $values]) + ->count(); + + $this->assertSame($expectedCount, $count); + + $count = $query + ->from('array_and_json_types') + ->where(['json overlaps', 'jsonb_col', $values]) + ->count(); + + $this->assertSame($expectedCount, $count); + + $db->close(); + } } diff --git a/tests/Support/Fixture/pgsql.sql b/tests/Support/Fixture/pgsql.sql index ece6899a6..b33d745b3 100644 --- a/tests/Support/Fixture/pgsql.sql +++ b/tests/Support/Fixture/pgsql.sql @@ -357,6 +357,10 @@ CREATE TABLE "array_and_json_types" ( jsonarray_col JSON[] ); +INSERT INTO "array_and_json_types" (intarray_col, json_col, jsonb_col) VALUES (null, null, null); +INSERT INTO "array_and_json_types" (intarray_col, json_col, jsonb_col) VALUES ('{1,2,3,null}', '[1,2,3,null]', '[1,2,3,null]'); +INSERT INTO "array_and_json_types" (intarray_col, json_col, jsonb_col) VALUES ('{3,4,5}', '[3,4,5]', '[3,4,5]'); + CREATE TABLE "T_constraints_1" ( "C_id" INT NOT NULL PRIMARY KEY, diff --git a/tests/Support/TestTrait.php b/tests/Support/TestTrait.php index 9b80c95d6..d58172805 100644 --- a/tests/Support/TestTrait.php +++ b/tests/Support/TestTrait.php @@ -64,4 +64,13 @@ protected function setFixture(string $fixture): void { $this->fixture = $fixture; } + + public static function setUpBeforeClass(): void + { + $db = self::getDb(); + + DbHelper::loadFixture($db, __DIR__ . '/Fixture/pgsql.sql'); + + $db->close(); + } } From 44c642e8693248e8566f23b36445057e989e042c Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Sat, 29 Jun 2024 07:53:23 +0000 Subject: [PATCH 2/5] Apply fixes from StyleCI --- tests/CommandTest.php | 1 - tests/Provider/QueryBuilderProvider.php | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/CommandTest.php b/tests/CommandTest.php index e008cbd43..7ef9c81d2 100644 --- a/tests/CommandTest.php +++ b/tests/CommandTest.php @@ -8,7 +8,6 @@ use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; -use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Pgsql\Connection; use Yiisoft\Db\Pgsql\Dsn; use Yiisoft\Db\Pgsql\Driver; diff --git a/tests/Provider/QueryBuilderProvider.php b/tests/Provider/QueryBuilderProvider.php index 3fc9a1e2d..bcadbe450 100644 --- a/tests/Provider/QueryBuilderProvider.php +++ b/tests/Provider/QueryBuilderProvider.php @@ -530,7 +530,7 @@ public static function overlapsCondition(): array $data['null'][1] = 0; $data['expression'][0] = new Expression("'{0,1,2,7}'"); - $data['query expression'][0] = (new Query(static::getDb()))->select(new ArrayExpression([0,1,2,7])); + $data['query expression'][0] = (new Query(self::getDb()))->select(new ArrayExpression([0,1,2,7])); $data[] = [new Expression('ARRAY[0,1,2,7]'), 1]; $data[] = [new ArrayExpression([0,1,2,7]), 1]; From 988dba81724d81970ec3d8f2b06cc2970f6da486 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 29 Jun 2024 17:13:18 +0700 Subject: [PATCH 3/5] Add line to CHANGELOG.md [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6abbcc859..0573645ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ for type casting performance. Related with yiisoft/db#752 (@Tigrov) - Chg #348: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov) - Enh #349: Add method chaining for column classes (@Tigrov) +- Enh #350: Add array and json overlaps condition builders (@Tigrov) ## 1.3.0 March 21, 2024 From 2d61b7832d3453870140bc03af217b3d524b150c Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sun, 30 Jun 2024 08:57:17 +0700 Subject: [PATCH 4/5] Improve --- src/Builder/JsonOverlapsConditionBuilder.php | 2 +- tests/QueryBuilderTest.php | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Builder/JsonOverlapsConditionBuilder.php b/src/Builder/JsonOverlapsConditionBuilder.php index 3f6565a37..dfb333c86 100644 --- a/src/Builder/JsonOverlapsConditionBuilder.php +++ b/src/Builder/JsonOverlapsConditionBuilder.php @@ -42,6 +42,6 @@ public function build(ExpressionInterface $expression, array &$params = []): str $values = $this->queryBuilder->buildExpression($values, $params); - return "ARRAY(SELECT jsonb_array_elements($column::jsonb))::text[] && $values::text[]"; + return "ARRAY(SELECT jsonb_array_elements_text($column::jsonb)) && $values::text[]"; } } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 3c87fabaf..28d571683 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -10,6 +10,7 @@ use Yiisoft\Db\Exception\IntegrityException; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Pgsql\Column; use Yiisoft\Db\Pgsql\Tests\Support\TestTrait; @@ -696,6 +697,13 @@ public function testArrayOverlapsConditionBuilder(): void $this->assertSame('"column"::text[] && ARRAY[:qp0, :qp1, :qp2]::text[]', $sql); $this->assertSame([':qp0' => 1, ':qp1' => 2, ':qp2' => 3], $params); + // Test column as Expression + $params = []; + $sql = $qb->buildExpression(new ArrayOverlapsCondition(new Expression('column'), [1, 2, 3]), $params); + + $this->assertSame('column::text[] && ARRAY[:qp0, :qp1, :qp2]::text[]', $sql); + $this->assertSame([':qp0' => 1, ':qp1' => 2, ':qp2' => 3], $params); + $db->close(); } @@ -708,7 +716,7 @@ public function testJsonOverlapsConditionBuilder(): void $sql = $qb->buildExpression(new JsonOverlapsCondition('column', [1, 2, 3]), $params); $this->assertSame( - 'ARRAY(SELECT jsonb_array_elements("column"::jsonb))::text[] && ARRAY[:qp0, :qp1, :qp2]::text[]', + 'ARRAY(SELECT jsonb_array_elements_text("column"::jsonb)) && ARRAY[:qp0, :qp1, :qp2]::text[]', $sql ); $this->assertSame([':qp0' => 1, ':qp1' => 2, ':qp2' => 3], $params); From 5ced70a3367f22131d30ad416afc41817e4efb86 Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Mon, 1 Jul 2024 16:36:36 +0700 Subject: [PATCH 5/5] Apply suggestions from code review [skip ci] Co-authored-by: Alexander Makarov --- CHANGELOG.md | 2 +- src/Builder/ArrayOverlapsConditionBuilder.php | 2 +- src/Builder/JsonOverlapsConditionBuilder.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0573645ed..1a1b2ccbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ for type casting performance. Related with yiisoft/db#752 (@Tigrov) - Chg #348: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov) - Enh #349: Add method chaining for column classes (@Tigrov) -- Enh #350: Add array and json overlaps condition builders (@Tigrov) +- Enh #350: Add array overlaps and JSON overlaps condition builders (@Tigrov) ## 1.3.0 March 21, 2024 diff --git a/src/Builder/ArrayOverlapsConditionBuilder.php b/src/Builder/ArrayOverlapsConditionBuilder.php index 41f0a3617..9db4ba010 100644 --- a/src/Builder/ArrayOverlapsConditionBuilder.php +++ b/src/Builder/ArrayOverlapsConditionBuilder.php @@ -22,7 +22,7 @@ final class ArrayOverlapsConditionBuilder extends AbstractOverlapsConditionBuild /** * Build SQL for {@see ArrayOverlapsCondition}. * - * @param ArrayOverlapsCondition $expression the {@see ArrayOverlapsCondition} to be built. + * @param ArrayOverlapsCondition $expression The {@see ArrayOverlapsCondition} to be built. * * @throws Exception * @throws InvalidArgumentException diff --git a/src/Builder/JsonOverlapsConditionBuilder.php b/src/Builder/JsonOverlapsConditionBuilder.php index dfb333c86..c6131946c 100644 --- a/src/Builder/JsonOverlapsConditionBuilder.php +++ b/src/Builder/JsonOverlapsConditionBuilder.php @@ -22,7 +22,7 @@ final class JsonOverlapsConditionBuilder extends AbstractOverlapsConditionBuilde /** * Build SQL for {@see JsonOverlapsCondition}. * - * @param JsonOverlapsCondition $expression the {@see JsonOverlapsCondition} to be built. + * @param JsonOverlapsCondition $expression The {@see JsonOverlapsCondition} to be built. * * @throws Exception * @throws InvalidArgumentException