Skip to content

Commit 67265cc

Browse files
feat: extend existing function support with optional boolean parameters (#347)
1 parent 0cda902 commit 67265cc

File tree

8 files changed

+223
-22
lines changed

8 files changed

+223
-22
lines changed

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayToJson.php

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,43 @@
44

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

7+
use Doctrine\ORM\Query\AST\Node;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\BooleanValidationTrait;
10+
711
/**
812
* Implementation of PostgreSQL ARRAY_TO_JSON().
913
*
10-
* @see https://www.postgresql.org/docs/9.6/static/functions-json.html
14+
* Returns the array as a JSON array. A PostgreSQL multidimensional array becomes a JSON array of arrays.
15+
* Line feeds will be added between dimension-1 elements if pretty_bool is true.
16+
*
17+
* @see https://www.postgresql.org/docs/16/functions-json.html
1118
* @since 0.10
1219
*
1320
* @author Martin Georgiev <martin.georgiev@gmail.com>
21+
*
22+
* @example Using it in DQL: "SELECT ARRAY_TO_JSON(e.array1) FROM Entity e"
23+
* @example Using it in DQL with pretty_bool: "SELECT ARRAY_TO_JSON(e.array1, 'true') FROM Entity e"
1424
*/
15-
class ArrayToJson extends BaseFunction
25+
class ArrayToJson extends BaseVariadicFunction
1626
{
27+
use BooleanValidationTrait;
28+
1729
protected function customizeFunction(): void
1830
{
1931
$this->setFunctionPrototype('array_to_json(%s)');
20-
$this->addNodeMapping('StringPrimary');
32+
}
33+
34+
protected function validateArguments(Node ...$arguments): void
35+
{
36+
$argumentCount = \count($arguments);
37+
if ($argumentCount < 1 || $argumentCount > 2) {
38+
throw InvalidArgumentForVariadicFunctionException::between('array_to_json', 1, 2);
39+
}
40+
41+
// Validate that the second parameter is a valid boolean if provided
42+
if ($argumentCount === 2) {
43+
$this->validateBoolean($arguments[1], 'ARRAY_TO_JSON');
44+
}
2145
}
2246
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbInsert.php

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,43 @@
44

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

7+
use Doctrine\ORM\Query\AST\Node;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\BooleanValidationTrait;
10+
711
/**
812
* Implementation of PostgreSQL JSONB_INSERT().
913
*
10-
* @see https://www.postgresql.org/docs/9.6/static/functions-array.html
14+
* Inserts a new value into a JSONB field at the specified path.
15+
* If the path already exists, the value is not changed unless the last parameter is true.
16+
*
17+
* @see https://www.postgresql.org/docs/16/functions-json.html
1118
* @since 0.10
1219
*
1320
* @author Martin Georgiev <martin.georgiev@gmail.com>
21+
*
22+
* @example Using it in DQL with path and value: "SELECT JSONB_INSERT(e.jsonbData, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') FROM Entity e"
23+
* @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"
1424
*/
15-
class JsonbInsert extends BaseFunction
25+
class JsonbInsert extends BaseVariadicFunction
1626
{
27+
use BooleanValidationTrait;
28+
1729
protected function customizeFunction(): void
1830
{
19-
$this->setFunctionPrototype('jsonb_insert(%s, %s, %s)');
20-
$this->addNodeMapping('StringPrimary');
21-
$this->addNodeMapping('StringPrimary');
22-
$this->addNodeMapping('StringPrimary');
31+
$this->setFunctionPrototype('jsonb_insert(%s)');
32+
}
33+
34+
protected function validateArguments(Node ...$arguments): void
35+
{
36+
$argumentCount = \count($arguments);
37+
if ($argumentCount < 3 || $argumentCount > 4) {
38+
throw InvalidArgumentForVariadicFunctionException::between('jsonb_insert', 3, 4);
39+
}
40+
41+
// Validate that the fourth parameter is a valid boolean if provided
42+
if ($argumentCount === 4) {
43+
$this->validateBoolean($arguments[3], 'JSONB_INSERT');
44+
}
2345
}
2446
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbSet.php

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,47 @@
44

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

7+
use Doctrine\ORM\Query\AST\Node;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\BooleanValidationTrait;
10+
711
/**
812
* Implementation of PostgreSQL JSONB_SET().
913
*
10-
* @see https://www.postgresql.org/docs/9.6/static/functions-array.html
14+
* Returns the target jsonb with the section designated by path replaced by the new value,
15+
* or with the new value added if create_missing is true (default is true) and the item
16+
* designated by path does not exist.
17+
*
18+
* As with the path orientated operators, negative integers that appear in path count from the end
19+
* of JSON arrays.
20+
*
21+
* @see https://www.postgresql.org/docs/16/functions-json.html
1122
* @since 0.10
1223
*
1324
* @author Martin Georgiev <martin.georgiev@gmail.com>
25+
*
26+
* @example Using it in DQL with path and value: "SELECT JSONB_SET(e.jsonbData, '{address,city}', '\"Sofia\"') FROM Entity e"
27+
* @example Using it in DQL with create_if_missing flag: "SELECT JSONB_SET(e.jsonbData, '{address,city}', '\"Sofia\"', false) FROM Entity e"
1428
*/
15-
class JsonbSet extends BaseFunction
29+
class JsonbSet extends BaseVariadicFunction
1630
{
31+
use BooleanValidationTrait;
32+
1733
protected function customizeFunction(): void
1834
{
19-
$this->setFunctionPrototype('jsonb_set(%s, %s, %s)');
20-
$this->addNodeMapping('StringPrimary');
21-
$this->addNodeMapping('StringPrimary');
22-
$this->addNodeMapping('StringPrimary');
35+
$this->setFunctionPrototype('jsonb_set(%s)');
36+
}
37+
38+
protected function validateArguments(Node ...$arguments): void
39+
{
40+
$argumentCount = \count($arguments);
41+
if ($argumentCount < 3 || $argumentCount > 4) {
42+
throw InvalidArgumentForVariadicFunctionException::between('jsonb_set', 3, 4);
43+
}
44+
45+
// Validate that the fourth parameter is a valid boolean if provided
46+
if ($argumentCount === 4) {
47+
$this->validateBoolean($arguments[3], 'JSONB_SET');
48+
}
2349
}
2450
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RowToJson.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,42 @@
44

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

7+
use Doctrine\ORM\Query\AST\Node;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Traits\BooleanValidationTrait;
10+
711
/**
812
* Implementation of PostgreSQL ROW_TO_JSON().
913
*
10-
* @see https://www.postgresql.org/docs/9.6/static/functions-json.html
14+
* Returns the row as a JSON object. Line feeds will be added between level-1 elements if pretty_bool is true.
15+
*
16+
* @see https://www.postgresql.org/docs/16/functions-json.html
1117
* @since 0.10
1218
*
1319
* @author Martin Georgiev <martin.georgiev@gmail.com>
20+
*
21+
* @example Using it in DQL: "SELECT ROW_TO_JSON(e.row) FROM Entity e"
22+
* @example Using it in DQL with pretty_bool: "SELECT ROW_TO_JSON(e.row, 'true') FROM Entity e"
1423
*/
15-
class RowToJson extends BaseFunction
24+
class RowToJson extends BaseVariadicFunction
1625
{
26+
use BooleanValidationTrait;
27+
1728
protected function customizeFunction(): void
1829
{
1930
$this->setFunctionPrototype('row_to_json(%s)');
20-
$this->addNodeMapping('StringPrimary');
31+
}
32+
33+
protected function validateArguments(Node ...$arguments): void
34+
{
35+
$argumentCount = \count($arguments);
36+
if ($argumentCount < 1 || $argumentCount > 2) {
37+
throw InvalidArgumentForVariadicFunctionException::between('row_to_json', 1, 2);
38+
}
39+
40+
// Validate that the second parameter is a valid boolean if provided
41+
if ($argumentCount === 2) {
42+
$this->validateBoolean($arguments[1], 'ROW_TO_JSON');
43+
}
2144
}
2245
}

tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ArrayToJsonTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66

77
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsArrays;
88
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayToJson;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
10+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidBooleanException;
911

1012
class ArrayToJsonTest extends TestCase
1113
{
@@ -20,13 +22,33 @@ protected function getExpectedSqlStatements(): array
2022
{
2123
return [
2224
'converts array to json' => 'SELECT array_to_json(c0_.array1) AS sclr_0 FROM ContainsArrays c0_',
25+
'converts array to json with pretty print' => "SELECT array_to_json(c0_.array1, 'true') AS sclr_0 FROM ContainsArrays c0_",
2326
];
2427
}
2528

2629
protected function getDqlStatements(): array
2730
{
2831
return [
2932
'converts array to json' => \sprintf('SELECT ARRAY_TO_JSON(e.array1) FROM %s e', ContainsArrays::class),
33+
'converts array to json with pretty print' => \sprintf("SELECT ARRAY_TO_JSON(e.array1, 'true') FROM %s e", ContainsArrays::class),
3034
];
3135
}
36+
37+
public function test_invalid_boolean_throws_exception(): void
38+
{
39+
$this->expectException(InvalidBooleanException::class);
40+
$this->expectExceptionMessage('Invalid boolean value "invalid" provided for ARRAY_TO_JSON. Must be "true" or "false".');
41+
42+
$dql = \sprintf("SELECT ARRAY_TO_JSON(e.array1, 'invalid') FROM %s e", ContainsArrays::class);
43+
$this->buildEntityManager()->createQuery($dql)->getSQL();
44+
}
45+
46+
public function test_too_many_arguments_throws_exception(): void
47+
{
48+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
49+
$this->expectExceptionMessage('array_to_json() requires between 1 and 2 arguments');
50+
51+
$dql = \sprintf("SELECT ARRAY_TO_JSON(e.array1, 'true', 'extra_arg') FROM %s e", ContainsArrays::class);
52+
$this->buildEntityManager()->createQuery($dql)->getSQL();
53+
}
3254
}

tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbInsertTest.php

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

77
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsJsons;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidBooleanException;
810
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbInsert;
911

1012
class JsonbInsertTest extends TestCase
@@ -19,14 +21,43 @@ protected function getStringFunctions(): array
1921
protected function getExpectedSqlStatements(): array
2022
{
2123
return [
22-
"SELECT jsonb_insert(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') AS sclr_0 FROM ContainsJsons c0_",
24+
'basic usage' => "SELECT jsonb_insert(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') AS sclr_0 FROM ContainsJsons c0_",
25+
'with create-if-missing parameter' => "SELECT jsonb_insert(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}', 'true') AS sclr_0 FROM ContainsJsons c0_",
2326
];
2427
}
2528

2629
protected function getDqlStatements(): array
2730
{
2831
return [
29-
\sprintf("SELECT JSONB_INSERT(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') FROM %s e", ContainsJsons::class),
32+
'basic usage' => \sprintf("SELECT JSONB_INSERT(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') FROM %s e", ContainsJsons::class),
33+
'with create-if-missing parameter' => \sprintf("SELECT JSONB_INSERT(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}', 'true') FROM %s e", ContainsJsons::class),
3034
];
3135
}
36+
37+
public function test_invalid_boolean_throws_exception(): void
38+
{
39+
$this->expectException(InvalidBooleanException::class);
40+
$this->expectExceptionMessage('Invalid boolean value "invalid" provided for JSONB_INSERT. Must be "true" or "false".');
41+
42+
$dql = \sprintf("SELECT JSONB_INSERT(e.object1, '{country}', '{\"iso_3166_a3_code\":\"bgr\"}', 'invalid') FROM %s e", ContainsJsons::class);
43+
$this->buildEntityManager()->createQuery($dql)->getSQL();
44+
}
45+
46+
public function test_too_few_arguments_throws_exception(): void
47+
{
48+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
49+
$this->expectExceptionMessage('jsonb_insert() requires between 3 and 4 arguments');
50+
51+
$dql = \sprintf('SELECT JSONB_INSERT(e.object1) FROM %s e', ContainsJsons::class);
52+
$this->buildEntityManager()->createQuery($dql)->getSQL();
53+
}
54+
55+
public function test_too_many_arguments_throws_exception(): void
56+
{
57+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
58+
$this->expectExceptionMessage('jsonb_insert() requires between 3 and 4 arguments');
59+
60+
$dql = \sprintf("SELECT JSONB_INSERT(e.object1, '{country}', '{\"iso_3166_a3_code\":\"bgr\"}', 'true', 'extra_arg') FROM %s e", ContainsJsons::class);
61+
$this->buildEntityManager()->createQuery($dql)->getSQL();
62+
}
3263
}

tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonbSetTest.php

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

77
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsJsons;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidBooleanException;
810
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbSet;
911

1012
class JsonbSetTest extends TestCase
@@ -19,14 +21,43 @@ protected function getStringFunctions(): array
1921
protected function getExpectedSqlStatements(): array
2022
{
2123
return [
22-
"SELECT jsonb_set(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') AS sclr_0 FROM ContainsJsons c0_",
24+
'basic usage' => "SELECT jsonb_set(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') AS sclr_0 FROM ContainsJsons c0_",
25+
'with create-if-missing parameter' => "SELECT jsonb_set(c0_.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}', 'false') AS sclr_0 FROM ContainsJsons c0_",
2326
];
2427
}
2528

2629
protected function getDqlStatements(): array
2730
{
2831
return [
29-
\sprintf("SELECT JSONB_SET(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') FROM %s e", ContainsJsons::class),
32+
'basic usage' => \sprintf("SELECT JSONB_SET(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}') FROM %s e", ContainsJsons::class),
33+
'with create-if-missing parameter' => \sprintf("SELECT JSONB_SET(e.object1, '{country}', '{\"iso_3166_a3_code\":\"BGR\"}', 'false') FROM %s e", ContainsJsons::class),
3034
];
3135
}
36+
37+
public function test_invalid_boolean_throws_exception(): void
38+
{
39+
$this->expectException(InvalidBooleanException::class);
40+
$this->expectExceptionMessage('Invalid boolean value "invalid" provided for JSONB_SET. Must be "true" or "false".');
41+
42+
$dql = \sprintf("SELECT JSONB_SET(e.object1, '{country}', '{\"iso_3166_a3_code\":\"bgr\"}', 'invalid') FROM %s e", ContainsJsons::class);
43+
$this->buildEntityManager()->createQuery($dql)->getSQL();
44+
}
45+
46+
public function test_too_few_arguments_throws_exception(): void
47+
{
48+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
49+
$this->expectExceptionMessage('jsonb_set() requires between 3 and 4 arguments');
50+
51+
$dql = \sprintf('SELECT JSONB_SET(e.object1) FROM %s e', ContainsJsons::class);
52+
$this->buildEntityManager()->createQuery($dql)->getSQL();
53+
}
54+
55+
public function test_too_many_arguments_throws_exception(): void
56+
{
57+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
58+
$this->expectExceptionMessage('jsonb_set() requires between 3 and 4 arguments');
59+
60+
$dql = \sprintf("SELECT JSONB_SET(e.object1, '{country}', '{\"iso_3166_a3_code\":\"bgr\"}', 'true', 'extra_arg') FROM %s e", ContainsJsons::class);
61+
$this->buildEntityManager()->createQuery($dql)->getSQL();
62+
}
3263
}

tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RowToJsonTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

77
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsTexts;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidBooleanException;
810
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RowToJson;
911

1012
class RowToJsonTest extends TestCase
@@ -21,6 +23,7 @@ protected function getExpectedSqlStatements(): array
2123
return [
2224
'converts row to json' => 'SELECT row_to_json(c0_.text1) AS sclr_0 FROM ContainsTexts c0_',
2325
'converts row with expression to json' => 'SELECT row_to_json(UPPER(c0_.text1)) AS sclr_0 FROM ContainsTexts c0_',
26+
'converts row to json with pretty print' => "SELECT row_to_json(c0_.text1, 'true') AS sclr_0 FROM ContainsTexts c0_",
2427
];
2528
}
2629

@@ -29,6 +32,25 @@ protected function getDqlStatements(): array
2932
return [
3033
'converts row to json' => \sprintf('SELECT ROW_TO_JSON(e.text1) FROM %s e', ContainsTexts::class),
3134
'converts row with expression to json' => \sprintf('SELECT ROW_TO_JSON(UPPER(e.text1)) FROM %s e', ContainsTexts::class),
35+
'converts row to json with pretty print' => \sprintf("SELECT ROW_TO_JSON(e.text1, 'true') FROM %s e", ContainsTexts::class),
3236
];
3337
}
38+
39+
public function test_invalid_boolean_throws_exception(): void
40+
{
41+
$this->expectException(InvalidBooleanException::class);
42+
$this->expectExceptionMessage('Invalid boolean value "invalid" provided for ROW_TO_JSON. Must be "true" or "false".');
43+
44+
$dql = \sprintf("SELECT ROW_TO_JSON(e.text1, 'invalid') FROM %s e", ContainsTexts::class);
45+
$this->buildEntityManager()->createQuery($dql)->getSQL();
46+
}
47+
48+
public function test_too_many_arguments_throws_exception(): void
49+
{
50+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
51+
$this->expectExceptionMessage('row_to_json() requires between 1 and 2 arguments');
52+
53+
$dql = \sprintf("SELECT ROW_TO_JSON(e.text1, 'true', 'extra_arg') FROM %s e", ContainsTexts::class);
54+
$this->buildEntityManager()->createQuery($dql)->getSQL();
55+
}
3456
}

0 commit comments

Comments
 (0)