Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception;

use Doctrine\DBAL\Types\ConversionException;

/**
* @since 3.1
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
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
));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;

use Doctrine\ORM\Query\AST\Node;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\BooleanValidationTrait;

/**
* Implementation of PostgreSQL JSONB_PATH_EXISTS().
*
* Checks whether the JSON path returns any item for the specified JSON value.
*
* @see https://www.postgresql.org/docs/14/functions-json.html
* @since 3.1
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*
* @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');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;

use Doctrine\ORM\Query\AST\Node;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\BooleanValidationTrait;

/**
* Implementation of PostgreSQL JSONB_PATH_MATCH().
*
* Returns the SQL boolean result of a JSON path predicate check for the specified JSON value.
* This is useful only with predicate check expressions, not SQL-standard JSON path expressions,
* since it will either fail or return NULL if the path result is not a single boolean value.
*
* @see https://www.postgresql.org/docs/14/functions-json.html
* @since 3.1
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*
* @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');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;

use Doctrine\ORM\Query\AST\Node;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\BooleanValidationTrait;

/**
* Implementation of PostgreSQL JSONB_PATH_QUERY().
*
* Returns all JSON items returned by the JSON path for the specified JSON value.
* Returns multiple rows, so it's useful when used in a subquery.
*
* @see https://www.postgresql.org/docs/14/functions-json.html
* @since 3.1
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*
* @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');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;

use Doctrine\ORM\Query\AST\Node;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\BooleanValidationTrait;

/**
* Implementation of PostgreSQL JSONB_PATH_QUERY_ARRAY().
*
* Returns all JSON items returned by the JSON path for the specified JSON value as a JSON array.
*
* @see https://www.postgresql.org/docs/14/functions-json.html
* @since 3.1
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*
* @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');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;

use Doctrine\ORM\Query\AST\Node;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\BooleanValidationTrait;

/**
* Implementation of PostgreSQL JSONB_PATH_QUERY_FIRST().
*
* Returns the first JSON item returned by the JSON path for the specified JSON value.
* Returns NULL if there are no results.
*
* @see https://www.postgresql.org/docs/14/functions-json.html
* @since 3.1
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*
* @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');
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits;

use Doctrine\ORM\Query\AST\Literal;
use Doctrine\ORM\Query\AST\Node;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidBooleanException;

/**
* Provides boolean validation functionality for functions that accept boolean parameters.
*
* @since 3.1
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types=1);

namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;

use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsJsons;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidBooleanException;
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbPathExists;

class JsonbPathExistsTest extends TestCase
{
protected function getStringFunctions(): array
{
return [
'JSONB_PATH_EXISTS' => 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();
}
}
Loading
Loading