Skip to content

Commit 4a26400

Browse files
feat: add (limited) support for json_exists, json_query, json_scalar, json_serialize and json_value (martin-georgiev#277)
1 parent 1d49d25 commit 4a26400

File tree

14 files changed

+387
-3
lines changed

14 files changed

+387
-3
lines changed

docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,15 @@
5353
| json_build_object | JSON_BUILD_OBJECT | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonBuildObject` |
5454
| json_each | JSON_EACH | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEach` |
5555
| json_each_text | JSON_EACH_TEXT | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEachText` |
56+
| json_exists | JSON_EXISTS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonExists` |
5657
| json_object_agg | JSON_OBJECT_AGG | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectAgg` |
5758
| json_object_keys | JSON_OBJECT_KEYS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectKeys` |
59+
| json_query | JSON_QUERY | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonQuery` |
60+
| json_scalar | JSON_SCALAR | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonScalar` |
61+
| json_serialize | JSON_SERIALIZE | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonSerialize` |
5862
| json_strip_nulls | JSON_STRIP_NULLS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonStripNulls` |
5963
| json_typeof | JSON_TYPEOF | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonTypeof` |
64+
| json_value | JSON_VALUE | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonValue` |
6065
| jsonb_array_elements | JSONB_ARRAY_ELEMENTS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbArrayElements` |
6166
| jsonb_agg | JSONB_AGG | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbAgg` |
6267
| jsonb_array_elements_text | JSONB_ARRAY_ELEMENTS_TEXT | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbArrayElementsText` |

docs/INTEGRATING-WITH-DOCTRINE.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,26 @@ use Doctrine\ORM\Configuration;
3333
$configuration = new Configuration();
3434

3535
# Register json functions
36+
$configuration->addCustomStringFunction('JSON_AGG', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonAgg::class);
37+
$configuration->addCustomStringFunction('JSON_ARRAY_LENGTH', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonArrayLength::class);
3638
$configuration->addCustomStringFunction('JSON_BUILD_OBJECT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonBuildObject::class);
37-
$configuration->addCustomStringFunction('JSONB_BUILD_OBJECT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbBuildObject::class);
38-
39+
$configuration->addCustomStringFunction('JSON_EACH', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEach::class);
40+
$configuration->addCustomStringFunction('JSON_EACH_TEXT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEachText::class);
41+
$configuration->addCustomStringFunction('JSON_EXISTS', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonExists::class);
42+
$configuration->addCustomStringFunction('JSON_GET_FIELD', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField::class);
43+
$configuration->addCustomStringFunction('JSON_GET_FIELD_AS_TEXT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsText::class);
44+
$configuration->addCustomStringFunction('JSON_GET_FIELD_AS_INTEGER', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsInteger::class);
45+
$configuration->addCustomStringFunction('JSON_GET_OBJECT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObject::class);
46+
$configuration->addCustomStringFunction('JSON_GET_OBJECT_AS_TEXT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObjectAsText::class);
47+
$configuration->addCustomStringFunction('JSON_OBJECT_AGG', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectAgg::class);
48+
$configuration->addCustomStringFunction('JSON_OBJECT_KEYS', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectKeys::class);
49+
$configuration->addCustomStringFunction('JSON_QUERY', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonQuery::class);
50+
$configuration->addCustomStringFunction('JSON_SCALAR', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonScalar::class);
51+
$configuration->addCustomStringFunction('JSON_SERIALIZE', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonSerialize::class);
52+
$configuration->addCustomStringFunction('JSON_STRIP_NULLS', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonStripNulls::class);
53+
$configuration->addCustomStringFunction('JSON_VALUE', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonValue::class);
54+
$configuration->addCustomStringFunction('TO_JSON', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToJson::class);
55+
$configuration->addCustomStringFunction('ROW_TO_JSON', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RowToJson::class);
3956
# Register text search functions
4057
$configuration->addCustomStringFunction('TO_TSQUERY', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToTsquery::class);
4158
$configuration->addCustomStringFunction('TO_TSVECTOR', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToTsvector::class);

docs/INTEGRATING-WITH-LARAVEL.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,19 @@ return [
111111
'JSON_BUILD_OBJECT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonBuildObject::class,
112112
'JSON_EACH' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEach::class,
113113
'JSON_EACH_TEXT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEachText::class,
114+
'JSON_EXISTS' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonExists::class,
114115
'JSON_GET_FIELD' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField::class,
115116
'JSON_GET_FIELD_AS_TEXT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsText::class,
116117
'JSON_GET_FIELD_AS_INTEGER' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsInteger::class,
117118
'JSON_GET_OBJECT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObject::class,
118119
'JSON_GET_OBJECT_AS_TEXT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObjectAsText::class,
119120
'JSON_OBJECT_AGG' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectAgg::class,
120121
'JSON_OBJECT_KEYS' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectKeys::class,
122+
'JSON_QUERY' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonQuery::class,
123+
'JSON_SCALAR' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonScalar::class,
124+
'JSON_SERIALIZE' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonSerialize::class,
121125
'JSON_STRIP_NULLS' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonStripNulls::class,
126+
'JSON_VALUE' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonValue::class,
122127
'TO_JSON' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToJson::class,
123128
'ROW_TO_JSON' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RowToJson::class,
124129

docs/INTEGRATING-WITH-SYMFONY.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,22 @@ doctrine:
104104
JSON_BUILD_OBJECT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonBuildObject
105105
JSON_EACH: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEach
106106
JSON_EACH_TEXT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEachText
107+
JSON_EXISTS: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonExists
107108
JSON_GET_FIELD: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField
108109
JSON_GET_FIELD_AS_INTEGER: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsInteger
109110
JSON_GET_FIELD_AS_TEXT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsText
110111
JSON_GET_OBJECT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObject
111112
JSON_GET_OBJECT_AS_TEXT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObjectAsText
112113
JSON_OBJECT_AGG: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectAgg
113114
JSON_OBJECT_KEYS: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectKeys
115+
JSON_QUERY: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonQuery
116+
JSON_SCALAR: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonScalar
117+
JSON_SERIALIZE: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonSerialize
114118
JSON_STRIP_NULLS: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonStripNulls
119+
JSON_TYPEOF: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonTypeof
120+
JSON_VALUE: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonValue
115121
TO_JSON: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToJson
116122
ROW_TO_JSON: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RowToJson
117-
JSON_TYPEOF: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonTypeof
118123
119124
# jsonb specific functions
120125
JSONB_AGG: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbAgg
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostgreSQL JSON_EXISTS().
9+
*
10+
* Supports basic form:
11+
* - json_exists(target_json, path_text)
12+
*
13+
* Note: The following variations are NOT supported due to DQL limitations:
14+
* - PASSING variables
15+
* - ON ERROR clause
16+
* - ON EMPTY clause
17+
* - WITH UNIQUE KEYS clause
18+
*
19+
* @see https://www.postgresql.org/docs/17/functions-json.html
20+
* @since 2.10
21+
*
22+
* @author Martin Georgiev <martin.georgiev@gmail.com>
23+
*/
24+
class JsonExists extends BaseFunction
25+
{
26+
protected function customizeFunction(): void
27+
{
28+
$this->setFunctionPrototype('json_exists(%s, %s)');
29+
$this->addNodeMapping('StringPrimary');
30+
$this->addNodeMapping('StringPrimary');
31+
}
32+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostgreSQL JSON_QUERY().
9+
*
10+
* Supports basic form:
11+
* - json_query(target_json, path_text)
12+
*
13+
* Note: The following variations are NOT supported due to DQL limitations:
14+
* - PASSING variables
15+
* - RETURNING type
16+
* - WRAPPER clause
17+
* - ON ERROR clause
18+
* - ON EMPTY clause
19+
* - WITH WRAPPER clause
20+
* - WITH CONDITIONAL WRAPPER clause
21+
* - EMPTY/NULL ON EMPTY
22+
* - ERROR/NULL ON ERROR
23+
*
24+
* @see https://www.postgresql.org/docs/17/functions-json.html
25+
* @since 2.10
26+
*
27+
* @author Martin Georgiev <martin.georgiev@gmail.com>
28+
*/
29+
class JsonQuery extends BaseFunction
30+
{
31+
protected function customizeFunction(): void
32+
{
33+
$this->setFunctionPrototype('json_query(%s, %s)');
34+
$this->addNodeMapping('StringPrimary');
35+
$this->addNodeMapping('StringPrimary');
36+
}
37+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostgreSQL JSON_SCALAR().
9+
*
10+
* Supports basic form:
11+
* - json_scalar(target_json)
12+
*
13+
* Note: The following variations are NOT supported due to DQL limitations:
14+
* - RETURNING type
15+
* - ON ERROR clause
16+
* - ERROR/NULL ON ERROR
17+
*
18+
* @see https://www.postgresql.org/docs/17/functions-json.html
19+
* @since 2.10
20+
*
21+
* @author Martin Georgiev <martin.georgiev@gmail.com>
22+
*/
23+
class JsonScalar extends BaseFunction
24+
{
25+
protected function customizeFunction(): void
26+
{
27+
$this->setFunctionPrototype('json_scalar(%s)');
28+
$this->addNodeMapping('StringPrimary');
29+
}
30+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostgreSQL JSON_SERIALIZE().
9+
*
10+
* Supports basic form:
11+
* - json_serialize(expression)
12+
*
13+
* Note: The following variations are NOT supported due to DQL limitations:
14+
* - RETURNING type
15+
* - FORMAT JSON clause
16+
* - ENCODING clause
17+
*
18+
* @see https://www.postgresql.org/docs/17/functions-json.html
19+
* @since 2.10
20+
*
21+
* @author Martin Georgiev <martin.georgiev@gmail.com>
22+
*/
23+
class JsonSerialize extends BaseFunction
24+
{
25+
protected function customizeFunction(): void
26+
{
27+
$this->setFunctionPrototype('json_serialize(%s)');
28+
$this->addNodeMapping('StringPrimary');
29+
}
30+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostgreSQL JSON_VALUE().
9+
*
10+
* Supports basic form:
11+
* - json_value(target_json, path_text)
12+
*
13+
* Note: The following variations are NOT supported due to DQL limitations:
14+
* - PASSING variables
15+
* - RETURNING type
16+
* - DEFAULT clause
17+
* - ON ERROR clause
18+
* - ON EMPTY clause
19+
* - ERROR/NULL/DEFAULT ON ERROR
20+
* - ERROR/NULL/DEFAULT ON EMPTY
21+
*
22+
* @see https://www.postgresql.org/docs/17/functions-json.html
23+
* @since 2.10
24+
*
25+
* @author Martin Georgiev <martin.georgiev@gmail.com>
26+
*/
27+
class JsonValue extends BaseFunction
28+
{
29+
protected function customizeFunction(): void
30+
{
31+
$this->setFunctionPrototype('json_value(%s, %s)');
32+
$this->addNodeMapping('StringPrimary');
33+
$this->addNodeMapping('StringPrimary');
34+
}
35+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsJsons;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonExists;
9+
10+
class JsonExistsTest extends TestCase
11+
{
12+
protected function getStringFunctions(): array
13+
{
14+
return [
15+
'JSON_EXISTS' => JsonExists::class,
16+
];
17+
}
18+
19+
protected function getExpectedSqlStatements(): array
20+
{
21+
return [
22+
// Basic usage
23+
"SELECT json_exists(c0_.object1, '$.name') AS sclr_0 FROM ContainsJsons c0_",
24+
// Nested path
25+
"SELECT json_exists(c0_.object1, '$.address.city') AS sclr_0 FROM ContainsJsons c0_",
26+
// Array element
27+
"SELECT json_exists(c0_.object1, '$.items[0]') AS sclr_0 FROM ContainsJsons c0_",
28+
];
29+
}
30+
31+
protected function getDqlStatements(): array
32+
{
33+
return [
34+
\sprintf("SELECT JSON_EXISTS(e.object1, '$.name') FROM %s e", ContainsJsons::class),
35+
\sprintf("SELECT JSON_EXISTS(e.object1, '$.address.city') FROM %s e", ContainsJsons::class),
36+
\sprintf("SELECT JSON_EXISTS(e.object1, '$.items[0]') FROM %s e", ContainsJsons::class),
37+
];
38+
}
39+
}

0 commit comments

Comments
 (0)