From 35a82f68ccf5e35de545a16abb959ac4f4f21049 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 29 Jun 2024 14:53:28 +0700 Subject: [PATCH 1/4] Add json overlaps condition builders --- src/Builder/JsonOverlapsConditionBuilder.php | 44 ++++++++++++++++++ src/DQLQueryBuilder.php | 3 ++ tests/QueryBuilderTest.php | 48 ++++++++++++++++++++ tests/Support/Fixture/sqlite.sql | 13 ++++++ 4 files changed, 108 insertions(+) create mode 100644 src/Builder/JsonOverlapsConditionBuilder.php diff --git a/src/Builder/JsonOverlapsConditionBuilder.php b/src/Builder/JsonOverlapsConditionBuilder.php new file mode 100644 index 00000000..ef6a3b4c --- /dev/null +++ b/src/Builder/JsonOverlapsConditionBuilder.php @@ -0,0 +1,44 @@ +prepareColumn($expression->getColumn()); + $values = $expression->getValues(); + + if (!$values instanceof ExpressionInterface) { + $values = new JsonExpression($values); + } + + $values = $this->queryBuilder->buildExpression($values, $params); + + return "EXISTS(SELECT value FROM json_each($column) INTERSECT SELECT value FROM json_each($values))=1"; + } +} diff --git a/src/DQLQueryBuilder.php b/src/DQLQueryBuilder.php index 7723feb9..2f4bed7c 100644 --- a/src/DQLQueryBuilder.php +++ b/src/DQLQueryBuilder.php @@ -12,10 +12,12 @@ use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\AbstractDQLQueryBuilder; use Yiisoft\Db\QueryBuilder\Condition\InCondition; +use Yiisoft\Db\QueryBuilder\Condition\JsonOverlapsCondition; use Yiisoft\Db\QueryBuilder\Condition\LikeCondition; use Yiisoft\Db\Sqlite\Builder\ExpressionBuilder; use Yiisoft\Db\Sqlite\Builder\InConditionBuilder; use Yiisoft\Db\Sqlite\Builder\JsonExpressionBuilder; +use Yiisoft\Db\Sqlite\Builder\JsonOverlapsConditionBuilder; use Yiisoft\Db\Sqlite\Builder\LikeConditionBuilder; use function array_filter; @@ -135,6 +137,7 @@ public function buildUnion(array $unions, array &$params = []): string protected function defaultExpressionBuilders(): array { return array_merge(parent::defaultExpressionBuilders(), [ + JsonOverlapsCondition::class => JsonOverlapsConditionBuilder::class, LikeCondition::class => LikeConditionBuilder::class, InCondition::class => InConditionBuilder::class, JsonExpression::class => JsonExpressionBuilder::class, diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index d9f58b9b..45027a42 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -13,6 +13,7 @@ use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Query\QueryInterface; +use Yiisoft\Db\QueryBuilder\Condition\JsonOverlapsCondition; use Yiisoft\Db\Schema\SchemaInterface; use Yiisoft\Db\Sqlite\Column; use Yiisoft\Db\Sqlite\Tests\Support\TestTrait; @@ -776,4 +777,51 @@ public function testSelectScalar(array|bool|float|int|string $columns, string $e { parent::testSelectScalar($columns, $expected); } + + public function testJsonOverlapsConditionBuilder(): void + { + $db = $this->getConnection(); + $qb = $db->getQueryBuilder(); + + $params = []; + $sql = $qb->buildExpression(new JsonOverlapsCondition('column', [1, 2, 3]), $params); + + $this->assertSame( + 'EXISTS(SELECT value FROM json_each(`column`) INTERSECT SELECT value FROM json_each(:qp0))=1', + $sql + ); + $this->assertSame([':qp0' => '[1,2,3]'], $params); + + $db->close(); + } + + /** @dataProvider \Yiisoft\Db\Sqlite\Tests\Provider\QueryBuilderProvider::overlapsCondition */ + public function testOverlapsCondition(iterable|ExpressionInterface $values, int $expectedCount): void + { + $db = $this->getConnection(true); + + $count = (new Query($db)) + ->from('json_type') + ->where(new JsonOverlapsCondition('json_col', $values)) + ->count(); + + $this->assertSame($expectedCount, $count); + + $db->close(); + } + + /** @dataProvider \Yiisoft\Db\Sqlite\Tests\Provider\QueryBuilderProvider::overlapsCondition */ + public function testOverlapsConditionOperator(iterable|ExpressionInterface $values, int $expectedCount): void + { + $db = $this->getConnection(true); + + $count = (new Query($db)) + ->from('json_type') + ->where(['json overlaps', 'json_col', $values]) + ->count(); + + $this->assertSame($expectedCount, $count); + + $db->close(); + } } diff --git a/tests/Support/Fixture/sqlite.sql b/tests/Support/Fixture/sqlite.sql index c55b2d4c..c083acbb 100644 --- a/tests/Support/Fixture/sqlite.sql +++ b/tests/Support/Fixture/sqlite.sql @@ -15,6 +15,8 @@ DROP TABLE IF EXISTS "negative_default_values"; DROP TABLE IF EXISTS "animal"; DROP TABLE IF EXISTS "default_pk"; DROP TABLE IF EXISTS "notauto_pk"; +DROP TABLE IF EXISTS "timestamp_default"; +DROP TABLE IF EXISTS "json_type"; DROP VIEW IF EXISTS "animal_view"; DROP TABLE IF EXISTS "T_constraints_4"; DROP TABLE IF EXISTS "T_constraints_3"; @@ -25,6 +27,7 @@ DROP TABLE IF EXISTS "T_upsert_1"; DROP TABLE IF EXISTS "T_constraints_check"; DROP TABLE IF EXISTS "foreign_keys_parent"; DROP TABLE IF EXISTS "foreign_keys_child"; +DROP TABLE IF EXISTS "json_type"; CREATE TABLE "profile" ( id INTEGER NOT NULL, @@ -173,6 +176,11 @@ CREATE TABLE "timestamp_default" ( timestamp_text TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP ); -- STRICT +CREATE TABLE "json_type" ( + id INTEGER PRIMARY KEY, + json_col JSON +); + CREATE VIEW "animal_view" AS SELECT * FROM "animal"; INSERT INTO "animal" ("type") VALUES ('yiiunit\data\ar\Cat'); @@ -216,6 +224,11 @@ INSERT INTO "order_item_with_null_fk" (order_id, item_id, quantity, subtotal) VA INSERT INTO "order_item_with_null_fk" (order_id, item_id, quantity, subtotal) VALUES (2, 3, 1, 8.0); INSERT INTO "order_item_with_null_fk" (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); +INSERT INTO "json_type" (json_col) VALUES (null); +INSERT INTO "json_type" (json_col) VALUES ('[]'); +INSERT INTO "json_type" (json_col) VALUES ('[1,2,3,null]'); +INSERT INTO "json_type" (json_col) VALUES ('[3,4,5]'); + /* bit test, see https://github.com/yiisoft/yii2/issues/9006 */ DROP TABLE IF EXISTS "bit_values"; From 70b8b9dee0a687f78348fe975755b3a2ec296be3 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 29 Jun 2024 16:40:20 +0700 Subject: [PATCH 2/4] Improve tests --- tests/QueryBuilderTest.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/QueryBuilderTest.php b/tests/QueryBuilderTest.php index 45027a42..44145585 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -9,6 +9,7 @@ use Yiisoft\Db\Exception\InvalidArgumentException; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; +use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Query\Query; @@ -792,11 +793,21 @@ public function testJsonOverlapsConditionBuilder(): void ); $this->assertSame([':qp0' => '[1,2,3]'], $params); + // Test column as Expression + $params = []; + $sql = $qb->buildExpression(new JsonOverlapsCondition(new Expression('column'), [1, 2, 3]), $params); + + $this->assertSame( + 'EXISTS(SELECT value FROM json_each(column) INTERSECT SELECT value FROM json_each(:qp0))=1', + $sql + ); + $this->assertSame([':qp0' => '[1,2,3]'], $params); + $db->close(); } /** @dataProvider \Yiisoft\Db\Sqlite\Tests\Provider\QueryBuilderProvider::overlapsCondition */ - public function testOverlapsCondition(iterable|ExpressionInterface $values, int $expectedCount): void + public function testJsonOverlapsCondition(iterable|ExpressionInterface $values, int $expectedCount): void { $db = $this->getConnection(true); @@ -811,7 +822,7 @@ public function testOverlapsCondition(iterable|ExpressionInterface $values, int } /** @dataProvider \Yiisoft\Db\Sqlite\Tests\Provider\QueryBuilderProvider::overlapsCondition */ - public function testOverlapsConditionOperator(iterable|ExpressionInterface $values, int $expectedCount): void + public function testJsonOverlapsConditionOperator(iterable|ExpressionInterface $values, int $expectedCount): void { $db = $this->getConnection(true); From 46819f8b4e7d6677707420071f5d942c080ec138 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 29 Jun 2024 17:14:19 +0700 Subject: [PATCH 3/4] Add line to CHANGELOG.md [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0db1647a..33b10509 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Enh #273: Implement `ColumnSchemaInterface` classes according to the data type of database table columns for type casting performance. Related with yiisoft/db#752 (@Tigrov) - Chg #307: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov) +- Enh #310: Add json overlaps condition builder (@Tigrov) ## 1.2.0 March 21, 2024 From 316c5500839c5777c349ceef19311405bcdfe566 Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Mon, 1 Jul 2024 16:38:26 +0700 Subject: [PATCH 4/4] Apply suggestions from code review [skip ci] Co-authored-by: Alexander Makarov --- CHANGELOG.md | 2 +- src/Builder/JsonOverlapsConditionBuilder.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33b10509..85c4d6fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ - Enh #273: Implement `ColumnSchemaInterface` classes according to the data type of database table columns for type casting performance. Related with yiisoft/db#752 (@Tigrov) - Chg #307: Replace call of `SchemaInterface::getRawTableName()` to `QuoterInterface::getRawTableName()` (@Tigrov) -- Enh #310: Add json overlaps condition builder (@Tigrov) +- Enh #310: Add JSON overlaps condition builder (@Tigrov) ## 1.2.0 March 21, 2024 diff --git a/src/Builder/JsonOverlapsConditionBuilder.php b/src/Builder/JsonOverlapsConditionBuilder.php index ef6a3b4c..f9725753 100644 --- a/src/Builder/JsonOverlapsConditionBuilder.php +++ b/src/Builder/JsonOverlapsConditionBuilder.php @@ -21,7 +21,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