diff --git a/CHANGELOG.md b/CHANGELOG.md index 0db1647a..85c4d6fc 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 diff --git a/src/Builder/JsonOverlapsConditionBuilder.php b/src/Builder/JsonOverlapsConditionBuilder.php new file mode 100644 index 00000000..f9725753 --- /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..44145585 100644 --- a/tests/QueryBuilderTest.php +++ b/tests/QueryBuilderTest.php @@ -9,10 +9,12 @@ 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; 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 +778,61 @@ 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); + + // 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 testJsonOverlapsCondition(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 testJsonOverlapsConditionOperator(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";