From 406928eb2ffd4de29f0a52fd407aed41a5547523 Mon Sep 17 00:00:00 2001 From: Maxim Smakouz Date: Thu, 23 Nov 2023 17:33:42 +0200 Subject: [PATCH] Fix `whereJsonContains` and `whereJsonDoesntContain` methods with a single column name (#143) --- .../Injection/CompileJsonContains.php | 6 ++++- .../Injection/PostgresJsonExpression.php | 19 ++++++++++++++- .../Driver/MySQL/Query/DeleteQueryTest.php | 20 ++++++++++++++++ .../Driver/MySQL/Query/SelectQueryTest.php | 22 +++++++++++++++++ .../Driver/MySQL/Query/UpdateQueryTest.php | 22 +++++++++++++++++ .../Driver/Postgres/Query/DeleteQueryTest.php | 20 ++++++++++++++++ .../Driver/Postgres/Query/SelectQueryTest.php | 22 +++++++++++++++++ .../Driver/Postgres/Query/UpdateQueryTest.php | 24 ++++++++++++++++++- tests/Database/Unit/Driver/JsonerTest.php | 2 +- 9 files changed, 153 insertions(+), 4 deletions(-) diff --git a/src/Driver/Postgres/Injection/CompileJsonContains.php b/src/Driver/Postgres/Injection/CompileJsonContains.php index fa62c76b..281016fb 100644 --- a/src/Driver/Postgres/Injection/CompileJsonContains.php +++ b/src/Driver/Postgres/Injection/CompileJsonContains.php @@ -17,7 +17,11 @@ protected function compile(string $statement): string { $path = $this->getPath($statement); $field = $this->getField($statement); - $attribute = $this->getAttribute($statement); + $attribute = $this->findAttribute($statement); + + if (empty($attribute)) { + return \sprintf('(%s)::jsonb @> ?', $field); + } if (!empty($path)) { return \sprintf('(%s->%s->%s)::jsonb @> ?', $field, $path, $attribute); diff --git a/src/Driver/Postgres/Injection/PostgresJsonExpression.php b/src/Driver/Postgres/Injection/PostgresJsonExpression.php index 30eff4ca..717aca04 100644 --- a/src/Driver/Postgres/Injection/PostgresJsonExpression.php +++ b/src/Driver/Postgres/Injection/PostgresJsonExpression.php @@ -40,10 +40,27 @@ protected function getPath(string $statement, string $quote = "'"): string * @return int|non-empty-string */ protected function getAttribute(string $statement): string|int + { + $attribute = $this->findAttribute($statement); + if ($attribute === null) { + throw new DriverException('Invalid statement. Unable to extract attribute.'); + } + + return $attribute; + } + + /** + * Returns the attribute (last part of the full path). Returns null if the attribute is not found. + * + * @param non-empty-string $statement + * + * @return int|non-empty-string|null + */ + protected function findAttribute(string $statement): string|int|null { $path = $this->getPathArray($statement); if ($path === []) { - throw new DriverException('Invalid statement. Unable to extract attribute.'); + return null; } $attribute = \array_pop($path); diff --git a/tests/Database/Functional/Driver/MySQL/Query/DeleteQueryTest.php b/tests/Database/Functional/Driver/MySQL/Query/DeleteQueryTest.php index defa93fa..8aa0536f 100644 --- a/tests/Database/Functional/Driver/MySQL/Query/DeleteQueryTest.php +++ b/tests/Database/Functional/Driver/MySQL/Query/DeleteQueryTest.php @@ -123,6 +123,16 @@ public function testDeleteWithWhereJsonContainsNested(): void $this->assertSameParameters([json_encode('+1234567890')], $select); } + public function testDeleteWithWhereJsonContainsSinglePath(): void + { + $select = $this->database + ->delete('table') + ->whereJsonContains('settings', []); + + $this->assertSameQuery('DELETE FROM {table} WHERE json_contains({settings}, ?)', $select); + $this->assertSameParameters([json_encode([])], $select); + } + public function testDeleteWithWhereJsonContainsArray(): void { $select = $this->database @@ -190,6 +200,16 @@ public function testDeleteWithWhereJsonDoesntContainNested(): void $this->assertSameParameters([json_encode('+1234567890')], $select); } + public function testDeleteWithWhereJsonDoesntContainSinglePath(): void + { + $select = $this->database + ->delete('table') + ->whereJsonDoesntContain('settings', []); + + $this->assertSameQuery('DELETE FROM {table} WHERE NOT json_contains({settings}, ?)', $select); + $this->assertSameParameters([json_encode([])], $select); + } + public function testDeleteWithWhereJsonDoesntContainArray(): void { $select = $this->database diff --git a/tests/Database/Functional/Driver/MySQL/Query/SelectQueryTest.php b/tests/Database/Functional/Driver/MySQL/Query/SelectQueryTest.php index 0ab03dd4..976f34eb 100644 --- a/tests/Database/Functional/Driver/MySQL/Query/SelectQueryTest.php +++ b/tests/Database/Functional/Driver/MySQL/Query/SelectQueryTest.php @@ -141,6 +141,17 @@ public function testSelectWithWhereJsonContainsNested(): void $this->assertSameParameters([json_encode('+1234567890')], $select); } + public function testSelectWithWhereJsonContainsSinglePath(): void + { + $select = $this->database + ->select() + ->from('table') + ->whereJsonContains('settings', []); + + $this->assertSameQuery('SELECT * FROM {table} WHERE json_contains({settings}, ?)', $select); + $this->assertSameParameters([json_encode([])], $select); + } + public function testSelectWithWhereJsonContainsArray(): void { $select = $this->database @@ -212,6 +223,17 @@ public function testSelectWithWhereJsonDoesntContainNested(): void $this->assertSameParameters([json_encode('+1234567890')], $select); } + public function testSelectWithWhereJsonDoesntContainSinglePath(): void + { + $select = $this->database + ->select() + ->from('table') + ->whereJsonDoesntContain('settings', []); + + $this->assertSameQuery('SELECT * FROM {table} WHERE NOT json_contains({settings}, ?)', $select); + $this->assertSameParameters([json_encode([])], $select); + } + public function testSelectWithWhereJsonDoesntContainArray(): void { $select = $this->database diff --git a/tests/Database/Functional/Driver/MySQL/Query/UpdateQueryTest.php b/tests/Database/Functional/Driver/MySQL/Query/UpdateQueryTest.php index 506c95a9..0761ab51 100644 --- a/tests/Database/Functional/Driver/MySQL/Query/UpdateQueryTest.php +++ b/tests/Database/Functional/Driver/MySQL/Query/UpdateQueryTest.php @@ -129,6 +129,17 @@ public function testUpdateWithWhereJsonContainsNested(): void $this->assertSameParameters(['value', json_encode('+1234567890')], $select); } + public function testUpdateWithWhereJsonContainsSinglePath(): void + { + $select = $this->database + ->update('table') + ->values(['some' => 'value']) + ->whereJsonContains('settings', []); + + $this->assertSameQuery('UPDATE {table} SET {some} = ? WHERE json_contains({settings}, ?)', $select); + $this->assertSameParameters(['value', json_encode([])], $select); + } + public function testUpdateWithWhereJsonContainsArray(): void { $select = $this->database @@ -200,6 +211,17 @@ public function testUpdateWithWhereJsonDoesntContainNested(): void $this->assertSameParameters(['value', json_encode('+1234567890')], $select); } + public function testUpdateWithWhereJsonDoesntContainSinglePath(): void + { + $select = $this->database + ->update('table') + ->values(['some' => 'value']) + ->whereJsonDoesntContain('settings', []); + + $this->assertSameQuery('UPDATE {table} SET {some} = ? WHERE NOT json_contains({settings}, ?)', $select); + $this->assertSameParameters(['value', json_encode([])], $select); + } + public function testUpdateWithWhereJsonDoesntContainArray(): void { $select = $this->database diff --git a/tests/Database/Functional/Driver/Postgres/Query/DeleteQueryTest.php b/tests/Database/Functional/Driver/Postgres/Query/DeleteQueryTest.php index 6fc49f9b..0ce531e1 100644 --- a/tests/Database/Functional/Driver/Postgres/Query/DeleteQueryTest.php +++ b/tests/Database/Functional/Driver/Postgres/Query/DeleteQueryTest.php @@ -111,6 +111,16 @@ public function testDeleteWithWhereJsonContainsNested(): void $this->assertSameParameters([json_encode('+1234567890')], $select); } + public function testDeleteWithWhereJsonContainsSinglePath(): void + { + $select = $this->database + ->delete('table') + ->whereJsonContains('settings', []); + + $this->assertSameQuery('DELETE FROM {table} WHERE ({settings})::jsonb @> ?', $select); + $this->assertSameParameters([json_encode([])], $select); + } + public function testDeleteWithWhereJsonContainsArray(): void { $select = $this->database @@ -178,6 +188,16 @@ public function testDeleteWithWhereJsonDoesntContainNested(): void $this->assertSameParameters([json_encode('+1234567890')], $select); } + public function testDeleteWithWhereJsonDoesntContainSinglePath(): void + { + $select = $this->database + ->delete('table') + ->whereJsonDoesntContain('settings', []); + + $this->assertSameQuery('DELETE FROM {table} WHERE NOT ({settings})::jsonb @> ?', $select); + $this->assertSameParameters([json_encode([])], $select); + } + public function testDeleteWithWhereJsonDoesntContainArray(): void { $select = $this->database diff --git a/tests/Database/Functional/Driver/Postgres/Query/SelectQueryTest.php b/tests/Database/Functional/Driver/Postgres/Query/SelectQueryTest.php index 1e5e8d57..5f6d6852 100644 --- a/tests/Database/Functional/Driver/Postgres/Query/SelectQueryTest.php +++ b/tests/Database/Functional/Driver/Postgres/Query/SelectQueryTest.php @@ -114,6 +114,17 @@ public function testSelectWithWhereJsonContainsNested(): void $this->assertSameParameters([json_encode('+1234567890')], $select); } + public function testSelectWithWhereJsonContainsSinglePath(): void + { + $select = $this->database + ->select() + ->from('table') + ->whereJsonContains('settings', []); + + $this->assertSameQuery('SELECT * FROM {table} WHERE ({settings})::jsonb @> ?', $select); + $this->assertSameParameters([json_encode([])], $select); + } + public function testSelectWithWhereJsonContainsArray(): void { $select = $this->database @@ -185,6 +196,17 @@ public function testSelectWithWhereJsonDoesntContainNested(): void $this->assertSameParameters([json_encode('+1234567890')], $select); } + public function testSelectWithWhereJsonDoesntContainSinglePath(): void + { + $select = $this->database + ->select() + ->from('table') + ->whereJsonDoesntContain('settings', []); + + $this->assertSameQuery('SELECT * FROM {table} WHERE NOT ({settings})::jsonb @> ?', $select); + $this->assertSameParameters([json_encode([])], $select); + } + public function testSelectWithWhereJsonDoesntContainArray(): void { $select = $this->database diff --git a/tests/Database/Functional/Driver/Postgres/Query/UpdateQueryTest.php b/tests/Database/Functional/Driver/Postgres/Query/UpdateQueryTest.php index 393582d0..8fd58e52 100644 --- a/tests/Database/Functional/Driver/Postgres/Query/UpdateQueryTest.php +++ b/tests/Database/Functional/Driver/Postgres/Query/UpdateQueryTest.php @@ -116,12 +116,23 @@ public function testUpdateWithWhereJsonContainsNested(): void ->whereJsonContains('settings->phones->work', '+1234567890'); $this->assertSameQuery( - "UPDATE {table} SET {some} = ? WHERE({settings}->'phones'->'work')::jsonb @> ?", + "UPDATE {table} SET {some} = ? WHERE ({settings}->'phones'->'work')::jsonb @> ?", $select ); $this->assertSameParameters(['value', json_encode('+1234567890')], $select); } + public function testUpdateWithWhereJsonContainsSinglePath(): void + { + $select = $this->database + ->update('table') + ->values(['some' => 'value']) + ->whereJsonContains('settings', []); + + $this->assertSameQuery('UPDATE {table} SET {some} = ? WHERE ({settings})::jsonb @> ?', $select); + $this->assertSameParameters(['value', json_encode([])], $select); + } + public function testUpdateWithWhereJsonContainsArray(): void { $select = $this->database @@ -194,6 +205,17 @@ public function testUpdateWithWhereJsonDoesntContainNested(): void $this->assertSameParameters(['value', json_encode('+1234567890')], $select); } + public function testUpdateWithWhereJsonDoesntContainSinglePath(): void + { + $select = $this->database + ->update('table') + ->values(['some' => 'value']) + ->whereJsonDoesntContain('settings', []); + + $this->assertSameQuery('UPDATE {table} SET {some} = ? WHERE NOT ({settings})::jsonb @> ?', $select); + $this->assertSameParameters(['value', json_encode([])], $select); + } + public function testUpdateWithWhereJsonDoesntContainArray(): void { $select = $this->database diff --git a/tests/Database/Unit/Driver/JsonerTest.php b/tests/Database/Unit/Driver/JsonerTest.php index a83c1ff4..add822af 100644 --- a/tests/Database/Unit/Driver/JsonerTest.php +++ b/tests/Database/Unit/Driver/JsonerTest.php @@ -60,7 +60,7 @@ public function __toString(): string // JsonSerializable object will be converted to JSON string correctly if 'encode' is set to 'true' yield [ new class () implements \JsonSerializable { - public function jsonSerialize() + public function jsonSerialize(): array { return ['foo' => 'bar']; }