From d4e2f398d22b5a4cf5599537acb15ea5faee6d26 Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Thu, 4 Jul 2024 09:55:42 +0700 Subject: [PATCH] Add json overlaps condition builder (#350) --- CHANGELOG.md | 1 + src/Builder/ArrayOverlapsConditionBuilder.php | 47 ++++++++ src/Builder/JsonOverlapsConditionBuilder.php | 47 ++++++++ src/DQLQueryBuilder.php | 6 ++ tests/CommandTest.php | 46 -------- tests/Provider/QueryBuilderProvider.php | 13 +++ tests/QueryBuilderTest.php | 102 ++++++++++++++++++ tests/Support/Fixture/pgsql.sql | 4 + tests/Support/TestTrait.php | 9 ++ 9 files changed, 229 insertions(+), 46 deletions(-) create mode 100644 src/Builder/ArrayOverlapsConditionBuilder.php create mode 100644 src/Builder/JsonOverlapsConditionBuilder.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6abbcc859..1a1b2ccbf 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 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 new file mode 100644 index 000000000..9db4ba010 --- /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..c6131946c --- /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_text($column::jsonb)) && $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..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; @@ -182,51 +181,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..bcadbe450 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(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]; + + return $data; + } } diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 6f8cd2481..28d571683 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -10,10 +10,14 @@ 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; +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 +685,102 @@ 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); + + // 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(); + } + + 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_text("column"::jsonb)) && 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(); + } }