From d96dbdaccf0512ca0dab807834d478b9b279929e Mon Sep 17 00:00:00 2001 From: Martin Georgiev Date: Sun, 13 Apr 2025 00:51:16 +0100 Subject: [PATCH] feat: add support for `JSONB_PATH_EXISTS()`, `JSONB_PATH_MATCH()`, `JSONB_PATH_QUERY()`, `JSONB_PATH_QUERY_ARRAY()` and `JSONB_PATH_QUERY_FIRST()` --- .../Exception/InvalidBooleanException.php | 33 +++++++++ .../Query/AST/Functions/JsonbPathExists.php | 44 ++++++++++++ .../Query/AST/Functions/JsonbPathMatch.php | 46 +++++++++++++ .../Query/AST/Functions/JsonbPathQuery.php | 45 +++++++++++++ .../AST/Functions/JsonbPathQueryArray.php | 44 ++++++++++++ .../AST/Functions/JsonbPathQueryFirst.php | 45 +++++++++++++ .../Traits/BooleanValidationTrait.php | 43 ++++++++++++ .../AST/Functions/JsonbPathExistsTest.php | 67 +++++++++++++++++++ .../AST/Functions/JsonbPathMatchTest.php | 67 +++++++++++++++++++ .../AST/Functions/JsonbPathQueryArrayTest.php | 67 +++++++++++++++++++ .../AST/Functions/JsonbPathQueryFirstTest.php | 67 +++++++++++++++++++ .../AST/Functions/JsonbPathQueryTest.php | 67 +++++++++++++++++++ 12 files changed, 635 insertions(+) create mode 100644 src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Exception/InvalidBooleanException.php create mode 100644 src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathExists.php create mode 100644 src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathMatch.php create mode 100644 src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQuery.php create mode 100644 src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryArray.php create mode 100644 src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryFirst.php create mode 100644 src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Traits/BooleanValidationTrait.php create mode 100644 tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathExistsTest.php create mode 100644 tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathMatchTest.php create mode 100644 tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryArrayTest.php create mode 100644 tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryFirstTest.php create mode 100644 tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryTest.php diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Exception/InvalidBooleanException.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Exception/InvalidBooleanException.php new file mode 100644 index 00000000..ea6670ce --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Exception/InvalidBooleanException.php @@ -0,0 +1,33 @@ + + */ +class InvalidBooleanException extends ConversionException +{ + public static function forNonLiteralNode(string $nodeClass, string $functionName): self + { + return new self(\sprintf( + 'The boolean parameter for %s must be a string literal, got %s', + $functionName, + $nodeClass + )); + } + + public static function forInvalidBoolean(string $value, string $functionName): self + { + return new self(\sprintf( + 'Invalid boolean value "%s" provided for %s. Must be "true" or "false".', + $value, + $functionName + )); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathExists.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathExists.php new file mode 100644 index 00000000..5d6a199d --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathExists.php @@ -0,0 +1,44 @@ + + * + * @example Using it in DQL: "SELECT JSONB_PATH_EXISTS(e.jsonbData, '$.a[*] ? (@ > 2)')" + */ +class JsonbPathExists extends BaseVariadicFunction +{ + use BooleanValidationTrait; + + protected function customizeFunction(): void + { + $this->setFunctionPrototype('jsonb_path_exists(%s)'); + } + + protected function validateArguments(Node ...$arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 2 || $argumentCount > 4) { + throw InvalidArgumentForVariadicFunctionException::between('jsonb_path_exists', 2, 4); + } + + // Validate that the fourth parameter is a valid boolean if provided + if ($argumentCount === 4) { + $this->validateBoolean($arguments[3], 'JSONB_PATH_EXISTS'); + } + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathMatch.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathMatch.php new file mode 100644 index 00000000..b5e052c7 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathMatch.php @@ -0,0 +1,46 @@ + + * + * @example Using it in DQL: "SELECT JSONB_PATH_MATCH(e.jsonbData, 'exists($.a[*] ? (@ >= 2 && @ <= 4))')" + */ +class JsonbPathMatch extends BaseVariadicFunction +{ + use BooleanValidationTrait; + + protected function customizeFunction(): void + { + $this->setFunctionPrototype('jsonb_path_match(%s)'); + } + + protected function validateArguments(Node ...$arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 2 || $argumentCount > 4) { + throw InvalidArgumentForVariadicFunctionException::between('jsonb_path_match', 2, 4); + } + + // Validate that the fourth parameter is a valid boolean if provided + if ($argumentCount === 4) { + $this->validateBoolean($arguments[3], 'JSONB_PATH_MATCH'); + } + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQuery.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQuery.php new file mode 100644 index 00000000..a88008c9 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQuery.php @@ -0,0 +1,45 @@ + + * + * @example Using it in DQL: "SELECT e.id FROM Entity e WHERE e.id IN (SELECT JSONB_PATH_QUERY(e2.jsonbData, '$.items[*].id') FROM Entity2 e2)" + */ +class JsonbPathQuery extends BaseVariadicFunction +{ + use BooleanValidationTrait; + + protected function customizeFunction(): void + { + $this->setFunctionPrototype('jsonb_path_query(%s)'); + } + + protected function validateArguments(Node ...$arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 2 || $argumentCount > 4) { + throw InvalidArgumentForVariadicFunctionException::between('jsonb_path_query', 2, 4); + } + + // Validate that the fourth parameter is a valid boolean if provided + if ($argumentCount === 4) { + $this->validateBoolean($arguments[3], 'JSONB_PATH_QUERY'); + } + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryArray.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryArray.php new file mode 100644 index 00000000..cb863046 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryArray.php @@ -0,0 +1,44 @@ + + * + * @example Using it in DQL: "SELECT JSONB_PATH_QUERY_ARRAY(e.jsonbData, '$.items[*].id') FROM Entity e" + */ +class JsonbPathQueryArray extends BaseVariadicFunction +{ + use BooleanValidationTrait; + + protected function customizeFunction(): void + { + $this->setFunctionPrototype('jsonb_path_query_array(%s)'); + } + + protected function validateArguments(Node ...$arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 2 || $argumentCount > 4) { + throw InvalidArgumentForVariadicFunctionException::between('jsonb_path_query_array', 2, 4); + } + + // Validate that the fourth parameter is a valid boolean if provided + if ($argumentCount === 4) { + $this->validateBoolean($arguments[3], 'JSONB_PATH_QUERY_ARRAY'); + } + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryFirst.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryFirst.php new file mode 100644 index 00000000..8c0d3bc2 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryFirst.php @@ -0,0 +1,45 @@ + + * + * @example Using it in DQL: "SELECT JSONB_PATH_QUERY_FIRST(e.jsonbData, '$.items[*] ? (@.price > 100)') FROM Entity e" + */ +class JsonbPathQueryFirst extends BaseVariadicFunction +{ + use BooleanValidationTrait; + + protected function customizeFunction(): void + { + $this->setFunctionPrototype('jsonb_path_query_first(%s)'); + } + + protected function validateArguments(Node ...$arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 2 || $argumentCount > 4) { + throw InvalidArgumentForVariadicFunctionException::between('jsonb_path_query_first', 2, 4); + } + + // Validate that the fourth parameter is a valid boolean if provided + if ($argumentCount === 4) { + $this->validateBoolean($arguments[3], 'JSONB_PATH_QUERY_FIRST'); + } + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Traits/BooleanValidationTrait.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Traits/BooleanValidationTrait.php new file mode 100644 index 00000000..a5904805 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/Traits/BooleanValidationTrait.php @@ -0,0 +1,43 @@ + + */ +trait BooleanValidationTrait +{ + /** + * Validates that the given node represents a valid boolean value. + * + * @throws InvalidBooleanException If the value is not a valid boolean + */ + protected function validateBoolean(Node $node, string $functionName): void + { + if (!$node instanceof Literal || !\is_string($node->value)) { + throw InvalidBooleanException::forNonLiteralNode($node::class, $functionName); + } + + $value = \strtolower(\trim((string) $node->value, "'\"")); + $lowercaseValue = \strtolower($value); + + if (!$this->isValidBoolean($lowercaseValue)) { + throw InvalidBooleanException::forInvalidBoolean($value, $functionName); + } + } + + private function isValidBoolean(string $boolean): bool + { + return \in_array($boolean, ['true', 'false'], true); + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathExistsTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathExistsTest.php new file mode 100644 index 00000000..b594a7ab --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathExistsTest.php @@ -0,0 +1,67 @@ + JsonbPathExists::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if path exists with condition' => "SELECT jsonb_path_exists(c0_.object1, '$.a[*] ? (@ > 2)') AS sclr_0 FROM ContainsJsons c0_", + 'checks if nested path exists' => "SELECT jsonb_path_exists(c0_.object1, '$.address.city') AS sclr_0 FROM ContainsJsons c0_", + 'checks if nested path exists with vars argument' => "SELECT jsonb_path_exists(c0_.object1, '$.address.city', '{\"strict\": false}') AS sclr_0 FROM ContainsJsons c0_", + 'checks if nested path exists with vars and silent arguments' => "SELECT jsonb_path_exists(c0_.object1, '$.address.city', '{\"strict\": false}', 'true') AS sclr_0 FROM ContainsJsons c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if path exists with condition' => \sprintf("SELECT JSONB_PATH_EXISTS(e.object1, '$.a[*] ? (@ > 2)') FROM %s e", ContainsJsons::class), + 'checks if nested path exists' => \sprintf("SELECT JSONB_PATH_EXISTS(e.object1, '$.address.city') FROM %s e", ContainsJsons::class), + 'checks if nested path exists with vars argument' => \sprintf("SELECT JSONB_PATH_EXISTS(e.object1, '$.address.city', '{\"strict\": false}') FROM %s e", ContainsJsons::class), + 'checks if nested path exists with vars and silent arguments' => \sprintf("SELECT JSONB_PATH_EXISTS(e.object1, '$.address.city', '{\"strict\": false}', 'true') FROM %s e", ContainsJsons::class), + ]; + } + + public function test_invalid_boolean_throws_exception(): void + { + $this->expectException(InvalidBooleanException::class); + $this->expectExceptionMessage('Invalid boolean value "invalid" provided for JSONB_PATH_EXISTS. Must be "true" or "false".'); + + $dql = \sprintf("SELECT JSONB_PATH_EXISTS(e.object1, '$.items[*].id', '{\"strict\": false}', 'invalid') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_few_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('jsonb_path_exists() requires between 2 and 4 arguments'); + + $dql = \sprintf('SELECT JSONB_PATH_EXISTS(e.object1) FROM %s e', ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_many_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('jsonb_path_exists() requires between 2 and 4 arguments'); + + $dql = \sprintf("SELECT JSONB_PATH_EXISTS(e.object1, '$.items[*].id', '{\"strict\": false}', 'true', 'extra_arg') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathMatchTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathMatchTest.php new file mode 100644 index 00000000..8bfb46b4 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathMatchTest.php @@ -0,0 +1,67 @@ + JsonbPathMatch::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'checks if path matches with exists condition' => "SELECT jsonb_path_match(c0_.object1, 'exists($.a[*] ? (@ >= 2 && @ <= 4))') AS sclr_0 FROM ContainsJsons c0_", + 'checks if path matches with simple condition' => "SELECT jsonb_path_match(c0_.object1, '$.a[*] > 2') AS sclr_0 FROM ContainsJsons c0_", + 'checks if path matches with simple condition and vars argument' => "SELECT jsonb_path_match(c0_.object1, '$.a[*] > 2', '{\"strict\": false}') AS sclr_0 FROM ContainsJsons c0_", + 'checks if path matches with simple condition and vars and silent arguments' => "SELECT jsonb_path_match(c0_.object1, '$.a[*] > 2', '{\"strict\": false}', 'true') AS sclr_0 FROM ContainsJsons c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'checks if path matches with exists condition' => \sprintf("SELECT JSONB_PATH_MATCH(e.object1, 'exists($.a[*] ? (@ >= 2 && @ <= 4))') FROM %s e", ContainsJsons::class), + 'checks if path matches with simple condition' => \sprintf("SELECT JSONB_PATH_MATCH(e.object1, '$.a[*] > 2') FROM %s e", ContainsJsons::class), + 'checks if path matches with simple condition and vars argument' => \sprintf("SELECT JSONB_PATH_MATCH(e.object1, '$.a[*] > 2', '{\"strict\": false}') FROM %s e", ContainsJsons::class), + 'checks if path matches with simple condition and vars and silent arguments' => \sprintf("SELECT JSONB_PATH_MATCH(e.object1, '$.a[*] > 2', '{\"strict\": false}', 'true') FROM %s e", ContainsJsons::class), + ]; + } + + public function test_invalid_boolean_throws_exception(): void + { + $this->expectException(InvalidBooleanException::class); + $this->expectExceptionMessage('Invalid boolean value "invalid" provided for JSONB_PATH_MATCH. Must be "true" or "false".'); + + $dql = \sprintf("SELECT JSONB_PATH_MATCH(e.object1, '$.items[*].id', '{\"strict\": false}', 'invalid') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_few_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('jsonb_path_match() requires between 2 and 4 arguments'); + + $dql = \sprintf('SELECT JSONB_PATH_MATCH(e.object1) FROM %s e', ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_many_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('jsonb_path_match() requires between 2 and 4 arguments'); + + $dql = \sprintf("SELECT JSONB_PATH_MATCH(e.object1, '$.items[*].id', '{\"strict\": false}', 'true', 'extra_arg') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryArrayTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryArrayTest.php new file mode 100644 index 00000000..440d21f7 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryArrayTest.php @@ -0,0 +1,67 @@ + JsonbPathQueryArray::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'extracts array with condition' => "SELECT jsonb_path_query_array(c0_.object1, '$.a[*] ? (@ > 2)') AS sclr_0 FROM ContainsJsons c0_", + 'extracts array of items' => "SELECT jsonb_path_query_array(c0_.object1, '$.items[*].id') AS sclr_0 FROM ContainsJsons c0_", + 'extracts array of items with vars argument' => "SELECT jsonb_path_query_array(c0_.object1, '$.items[*].id', '{\"strict\": false}') AS sclr_0 FROM ContainsJsons c0_", + 'extracts array of items with vars and silent arguments' => "SELECT jsonb_path_query_array(c0_.object1, '$.items[*].id', '{\"strict\": false}', 'true') AS sclr_0 FROM ContainsJsons c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'extracts array with condition' => \sprintf("SELECT JSONB_PATH_QUERY_ARRAY(e.object1, '$.a[*] ? (@ > 2)') FROM %s e", ContainsJsons::class), + 'extracts array of items' => \sprintf("SELECT JSONB_PATH_QUERY_ARRAY(e.object1, '$.items[*].id') FROM %s e", ContainsJsons::class), + 'extracts array of items with vars argument' => \sprintf("SELECT JSONB_PATH_QUERY_ARRAY(e.object1, '$.items[*].id', '{\"strict\": false}') FROM %s e", ContainsJsons::class), + 'extracts array of items with vars and silent arguments' => \sprintf("SELECT JSONB_PATH_QUERY_ARRAY(e.object1, '$.items[*].id', '{\"strict\": false}', 'true') FROM %s e", ContainsJsons::class), + ]; + } + + public function test_invalid_boolean_throws_exception(): void + { + $this->expectException(InvalidBooleanException::class); + $this->expectExceptionMessage('Invalid boolean value "invalid" provided for JSONB_PATH_QUERY_ARRAY. Must be "true" or "false".'); + + $dql = \sprintf("SELECT JSONB_PATH_QUERY_ARRAY(e.object1, '$.items[*].id', '{\"strict\": false}', 'invalid') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_few_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('jsonb_path_query_array() requires between 2 and 4 arguments'); + + $dql = \sprintf('SELECT JSONB_PATH_QUERY_ARRAY(e.object1) FROM %s e', ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_many_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('jsonb_path_query_array() requires between 2 and 4 arguments'); + + $dql = \sprintf("SELECT JSONB_PATH_QUERY_ARRAY(e.object1, '$.items[*].id', '{\"strict\": false}', 'true', 'extra_arg') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryFirstTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryFirstTest.php new file mode 100644 index 00000000..9a467ab5 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryFirstTest.php @@ -0,0 +1,67 @@ + JsonbPathQueryFirst::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'extracts first item with condition' => "SELECT jsonb_path_query_first(c0_.object1, '$.items[*] ? (@.price > 100)') AS sclr_0 FROM ContainsJsons c0_", + 'extracts first item from path' => "SELECT jsonb_path_query_first(c0_.object1, '$.items[*].id') AS sclr_0 FROM ContainsJsons c0_", + 'with vars argument' => "SELECT jsonb_path_query_first(c0_.object1, '$.items[*].id', '{\"strict\": false}') AS sclr_0 FROM ContainsJsons c0_", + 'with vars and silent arguments' => "SELECT jsonb_path_query_first(c0_.object1, '$.items[*].id', '{\"strict\": false}', 'true') AS sclr_0 FROM ContainsJsons c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'extracts first item with condition' => \sprintf("SELECT JSONB_PATH_QUERY_FIRST(e.object1, '$.items[*] ? (@.price > 100)') FROM %s e", ContainsJsons::class), + 'extracts first item from path' => \sprintf("SELECT JSONB_PATH_QUERY_FIRST(e.object1, '$.items[*].id') FROM %s e", ContainsJsons::class), + 'with vars argument' => \sprintf("SELECT JSONB_PATH_QUERY_FIRST(e.object1, '$.items[*].id', '{\"strict\": false}') FROM %s e", ContainsJsons::class), + 'with vars and silent arguments' => \sprintf("SELECT JSONB_PATH_QUERY_FIRST(e.object1, '$.items[*].id', '{\"strict\": false}', 'true') FROM %s e", ContainsJsons::class), + ]; + } + + public function test_invalid_boolean_throws_exception(): void + { + $this->expectException(InvalidBooleanException::class); + $this->expectExceptionMessage('Invalid boolean value "invalid" provided for JSONB_PATH_QUERY_FIRST. Must be "true" or "false".'); + + $dql = \sprintf("SELECT JSONB_PATH_QUERY_FIRST(e.object1, '$.items[*].id', '{\"strict\": false}', 'invalid') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_few_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('jsonb_path_query_first() requires between 2 and 4 arguments'); + + $dql = \sprintf('SELECT JSONB_PATH_QUERY_FIRST(e.object1) FROM %s e', ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_many_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('jsonb_path_query_first() requires between 2 and 4 arguments'); + + $dql = \sprintf("SELECT JSONB_PATH_QUERY_FIRST(e.object1, '$.items[*].id', '{\"strict\": false}', 'true', 'extra_arg') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryTest.php new file mode 100644 index 00000000..1b470eb1 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbPathQueryTest.php @@ -0,0 +1,67 @@ + JsonbPathQuery::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'extracts items with condition' => "SELECT jsonb_path_query(c0_.object1, '$.items[*] ? (@.price > 100)') AS sclr_0 FROM ContainsJsons c0_", + 'extracts items from path' => "SELECT jsonb_path_query(c0_.object1, '$.items[*].id') AS sclr_0 FROM ContainsJsons c0_", + 'extracts items from path with vars argument' => "SELECT jsonb_path_query(c0_.object1, '$.items[*].id', '{\"strict\": false}') AS sclr_0 FROM ContainsJsons c0_", + 'extracts items from path with vars and silent arguments' => "SELECT jsonb_path_query(c0_.object1, '$.items[*].id', '{\"strict\": false}', 'true') AS sclr_0 FROM ContainsJsons c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'extracts items with condition' => \sprintf("SELECT JSONB_PATH_QUERY(e.object1, '$.items[*] ? (@.price > 100)') FROM %s e", ContainsJsons::class), + 'extracts items from path' => \sprintf("SELECT JSONB_PATH_QUERY(e.object1, '$.items[*].id') FROM %s e", ContainsJsons::class), + 'extracts items from path with vars argument' => \sprintf("SELECT JSONB_PATH_QUERY(e.object1, '$.items[*].id', '{\"strict\": false}') FROM %s e", ContainsJsons::class), + 'extracts items from path with vars and silent arguments' => \sprintf("SELECT JSONB_PATH_QUERY(e.object1, '$.items[*].id', '{\"strict\": false}', 'true') FROM %s e", ContainsJsons::class), + ]; + } + + public function test_invalid_boolean_throws_exception(): void + { + $this->expectException(InvalidBooleanException::class); + $this->expectExceptionMessage('Invalid boolean value "invalid" provided for JSONB_PATH_QUERY. Must be "true" or "false".'); + + $dql = \sprintf("SELECT JSONB_PATH_QUERY(e.object1, '$.items[*].id', '{\"strict\": false}', 'invalid') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_few_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('jsonb_path_query() requires between 2 and 4 arguments'); + + $dql = \sprintf('SELECT JSONB_PATH_QUERY(e.object1) FROM %s e', ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_many_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('jsonb_path_query() requires between 2 and 4 arguments'); + + $dql = \sprintf("SELECT JSONB_PATH_QUERY(e.object1, '$.items[*].id', '{\"strict\": false}', 'true', 'extra_arg') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } +}