diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayToJson.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayToJson.php index 8e452144..4f20134e 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayToJson.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayToJson.php @@ -4,19 +4,43 @@ 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 ARRAY_TO_JSON(). * - * @see https://www.postgresql.org/docs/9.6/static/functions-json.html + * Returns the array as a JSON array. A PostgreSQL multidimensional array becomes a JSON array of arrays. + * Line feeds will be added between dimension-1 elements if pretty_bool is true. + * + * @see https://www.postgresql.org/docs/16/functions-json.html * @since 0.10 * * @author Martin Georgiev + * + * @example Using it in DQL: "SELECT ARRAY_TO_JSON(e.array1) FROM Entity e" + * @example Using it in DQL with pretty_bool: "SELECT ARRAY_TO_JSON(e.array1, 'true') FROM Entity e" */ -class ArrayToJson extends BaseFunction +class ArrayToJson extends BaseVariadicFunction { + use BooleanValidationTrait; + protected function customizeFunction(): void { $this->setFunctionPrototype('array_to_json(%s)'); - $this->addNodeMapping('StringPrimary'); + } + + protected function validateArguments(Node ...$arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 1 || $argumentCount > 2) { + throw InvalidArgumentForVariadicFunctionException::between('array_to_json', 1, 2); + } + + // Validate that the second parameter is a valid boolean if provided + if ($argumentCount === 2) { + $this->validateBoolean($arguments[1], 'ARRAY_TO_JSON'); + } } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbInsert.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbInsert.php index 669cb51f..855ebb09 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbInsert.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbInsert.php @@ -4,21 +4,43 @@ 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_INSERT(). * - * @see https://www.postgresql.org/docs/9.6/static/functions-array.html + * Inserts a new value into a JSONB field at the specified path. + * If the path already exists, the value is not changed unless the last parameter is true. + * + * @see https://www.postgresql.org/docs/16/functions-json.html * @since 0.10 * * @author Martin Georgiev + * + * @example Using it in DQL with path and value: "SELECT JSONB_INSERT(e.jsonbData, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') FROM Entity e" + * @example Using it in DQL with create_if_missing flag: "SELECT JSONB_INSERT(e.jsonbData, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}', true) FROM Entity e" */ -class JsonbInsert extends BaseFunction +class JsonbInsert extends BaseVariadicFunction { + use BooleanValidationTrait; + protected function customizeFunction(): void { - $this->setFunctionPrototype('jsonb_insert(%s, %s, %s)'); - $this->addNodeMapping('StringPrimary'); - $this->addNodeMapping('StringPrimary'); - $this->addNodeMapping('StringPrimary'); + $this->setFunctionPrototype('jsonb_insert(%s)'); + } + + protected function validateArguments(Node ...$arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 3 || $argumentCount > 4) { + throw InvalidArgumentForVariadicFunctionException::between('jsonb_insert', 3, 4); + } + + // Validate that the fourth parameter is a valid boolean if provided + if ($argumentCount === 4) { + $this->validateBoolean($arguments[3], 'JSONB_INSERT'); + } } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbSet.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbSet.php index f69c35a5..2626ccec 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbSet.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbSet.php @@ -4,21 +4,47 @@ 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_SET(). * - * @see https://www.postgresql.org/docs/9.6/static/functions-array.html + * Returns the target jsonb with the section designated by path replaced by the new value, + * or with the new value added if create_missing is true (default is true) and the item + * designated by path does not exist. + * + * As with the path orientated operators, negative integers that appear in path count from the end + * of JSON arrays. + * + * @see https://www.postgresql.org/docs/16/functions-json.html * @since 0.10 * * @author Martin Georgiev + * + * @example Using it in DQL with path and value: "SELECT JSONB_SET(e.jsonbData, '{address,city}', '\"Sofia\"') FROM Entity e" + * @example Using it in DQL with create_if_missing flag: "SELECT JSONB_SET(e.jsonbData, '{address,city}', '\"Sofia\"', false) FROM Entity e" */ -class JsonbSet extends BaseFunction +class JsonbSet extends BaseVariadicFunction { + use BooleanValidationTrait; + protected function customizeFunction(): void { - $this->setFunctionPrototype('jsonb_set(%s, %s, %s)'); - $this->addNodeMapping('StringPrimary'); - $this->addNodeMapping('StringPrimary'); - $this->addNodeMapping('StringPrimary'); + $this->setFunctionPrototype('jsonb_set(%s)'); + } + + protected function validateArguments(Node ...$arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 3 || $argumentCount > 4) { + throw InvalidArgumentForVariadicFunctionException::between('jsonb_set', 3, 4); + } + + // Validate that the fourth parameter is a valid boolean if provided + if ($argumentCount === 4) { + $this->validateBoolean($arguments[3], 'JSONB_SET'); + } } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RowToJson.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RowToJson.php index 7152db7b..a6488259 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RowToJson.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RowToJson.php @@ -4,19 +4,42 @@ 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 ROW_TO_JSON(). * - * @see https://www.postgresql.org/docs/9.6/static/functions-json.html + * Returns the row as a JSON object. Line feeds will be added between level-1 elements if pretty_bool is true. + * + * @see https://www.postgresql.org/docs/16/functions-json.html * @since 0.10 * * @author Martin Georgiev + * + * @example Using it in DQL: "SELECT ROW_TO_JSON(e.row) FROM Entity e" + * @example Using it in DQL with pretty_bool: "SELECT ROW_TO_JSON(e.row, 'true') FROM Entity e" */ -class RowToJson extends BaseFunction +class RowToJson extends BaseVariadicFunction { + use BooleanValidationTrait; + protected function customizeFunction(): void { $this->setFunctionPrototype('row_to_json(%s)'); - $this->addNodeMapping('StringPrimary'); + } + + protected function validateArguments(Node ...$arguments): void + { + $argumentCount = \count($arguments); + if ($argumentCount < 1 || $argumentCount > 2) { + throw InvalidArgumentForVariadicFunctionException::between('row_to_json', 1, 2); + } + + // Validate that the second parameter is a valid boolean if provided + if ($argumentCount === 2) { + $this->validateBoolean($arguments[1], 'ROW_TO_JSON'); + } } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayToJsonTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayToJsonTest.php index dfa7e7af..987c0e3c 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayToJsonTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayToJsonTest.php @@ -6,6 +6,8 @@ use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsArrays; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayToJson; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidBooleanException; class ArrayToJsonTest extends TestCase { @@ -20,6 +22,7 @@ protected function getExpectedSqlStatements(): array { return [ 'converts array to json' => 'SELECT array_to_json(c0_.array1) AS sclr_0 FROM ContainsArrays c0_', + 'converts array to json with pretty print' => "SELECT array_to_json(c0_.array1, 'true') AS sclr_0 FROM ContainsArrays c0_", ]; } @@ -27,6 +30,25 @@ protected function getDqlStatements(): array { return [ 'converts array to json' => \sprintf('SELECT ARRAY_TO_JSON(e.array1) FROM %s e', ContainsArrays::class), + 'converts array to json with pretty print' => \sprintf("SELECT ARRAY_TO_JSON(e.array1, 'true') FROM %s e", ContainsArrays::class), ]; } + + public function test_invalid_boolean_throws_exception(): void + { + $this->expectException(InvalidBooleanException::class); + $this->expectExceptionMessage('Invalid boolean value "invalid" provided for ARRAY_TO_JSON. Must be "true" or "false".'); + + $dql = \sprintf("SELECT ARRAY_TO_JSON(e.array1, 'invalid') FROM %s e", ContainsArrays::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_many_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('array_to_json() requires between 1 and 2 arguments'); + + $dql = \sprintf("SELECT ARRAY_TO_JSON(e.array1, 'true', 'extra_arg') FROM %s e", ContainsArrays::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbInsertTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbInsertTest.php index 27c875f3..be2e3160 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbInsertTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbInsertTest.php @@ -5,6 +5,8 @@ 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\JsonbInsert; class JsonbInsertTest extends TestCase @@ -19,14 +21,43 @@ protected function getStringFunctions(): array protected function getExpectedSqlStatements(): array { return [ - "SELECT jsonb_insert(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') AS sclr_0 FROM ContainsJsons c0_", + 'basic usage' => "SELECT jsonb_insert(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') AS sclr_0 FROM ContainsJsons c0_", + 'with create-if-missing parameter' => "SELECT jsonb_insert(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}', 'true') AS sclr_0 FROM ContainsJsons c0_", ]; } protected function getDqlStatements(): array { return [ - \sprintf("SELECT JSONB_INSERT(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') FROM %s e", ContainsJsons::class), + 'basic usage' => \sprintf("SELECT JSONB_INSERT(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') FROM %s e", ContainsJsons::class), + 'with create-if-missing parameter' => \sprintf("SELECT JSONB_INSERT(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}', '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_INSERT. Must be "true" or "false".'); + + $dql = \sprintf("SELECT JSONB_INSERT(e.object1, '{country}', '{\"iso_3166_a3_code\":\"bgr\"}', '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_insert() requires between 3 and 4 arguments'); + + $dql = \sprintf('SELECT JSONB_INSERT(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_insert() requires between 3 and 4 arguments'); + + $dql = \sprintf("SELECT JSONB_INSERT(e.object1, '{country}', '{\"iso_3166_a3_code\":\"bgr\"}', 'true', 'extra_arg') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbSetTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbSetTest.php index 9620300c..edbcf998 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbSetTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbSetTest.php @@ -5,6 +5,8 @@ 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\JsonbSet; class JsonbSetTest extends TestCase @@ -19,14 +21,43 @@ protected function getStringFunctions(): array protected function getExpectedSqlStatements(): array { return [ - "SELECT jsonb_set(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') AS sclr_0 FROM ContainsJsons c0_", + 'basic usage' => "SELECT jsonb_set(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') AS sclr_0 FROM ContainsJsons c0_", + 'with create-if-missing parameter' => "SELECT jsonb_set(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}', 'false') AS sclr_0 FROM ContainsJsons c0_", ]; } protected function getDqlStatements(): array { return [ - \sprintf("SELECT JSONB_SET(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') FROM %s e", ContainsJsons::class), + 'basic usage' => \sprintf("SELECT JSONB_SET(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') FROM %s e", ContainsJsons::class), + 'with create-if-missing parameter' => \sprintf("SELECT JSONB_SET(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}', 'false') 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_SET. Must be "true" or "false".'); + + $dql = \sprintf("SELECT JSONB_SET(e.object1, '{country}', '{\"iso_3166_a3_code\":\"bgr\"}', '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_set() requires between 3 and 4 arguments'); + + $dql = \sprintf('SELECT JSONB_SET(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_set() requires between 3 and 4 arguments'); + + $dql = \sprintf("SELECT JSONB_SET(e.object1, '{country}', '{\"iso_3166_a3_code\":\"bgr\"}', 'true', 'extra_arg') FROM %s e", ContainsJsons::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RowToJsonTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RowToJsonTest.php index 059ab60f..74a64080 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RowToJsonTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RowToJsonTest.php @@ -5,6 +5,8 @@ namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsTexts; +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\RowToJson; class RowToJsonTest extends TestCase @@ -21,6 +23,7 @@ protected function getExpectedSqlStatements(): array return [ 'converts row to json' => 'SELECT row_to_json(c0_.text1) AS sclr_0 FROM ContainsTexts c0_', 'converts row with expression to json' => 'SELECT row_to_json(UPPER(c0_.text1)) AS sclr_0 FROM ContainsTexts c0_', + 'converts row to json with pretty print' => "SELECT row_to_json(c0_.text1, 'true') AS sclr_0 FROM ContainsTexts c0_", ]; } @@ -29,6 +32,25 @@ protected function getDqlStatements(): array return [ 'converts row to json' => \sprintf('SELECT ROW_TO_JSON(e.text1) FROM %s e', ContainsTexts::class), 'converts row with expression to json' => \sprintf('SELECT ROW_TO_JSON(UPPER(e.text1)) FROM %s e', ContainsTexts::class), + 'converts row to json with pretty print' => \sprintf("SELECT ROW_TO_JSON(e.text1, 'true') FROM %s e", ContainsTexts::class), ]; } + + public function test_invalid_boolean_throws_exception(): void + { + $this->expectException(InvalidBooleanException::class); + $this->expectExceptionMessage('Invalid boolean value "invalid" provided for ROW_TO_JSON. Must be "true" or "false".'); + + $dql = \sprintf("SELECT ROW_TO_JSON(e.text1, 'invalid') FROM %s e", ContainsTexts::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_many_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('row_to_json() requires between 1 and 2 arguments'); + + $dql = \sprintf("SELECT ROW_TO_JSON(e.text1, 'true', 'extra_arg') FROM %s e", ContainsTexts::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } }