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
5 changes: 5 additions & 0 deletions docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,15 @@
| json_build_object | JSON_BUILD_OBJECT | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonBuildObject` |
| json_each | JSON_EACH | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEach` |
| json_each_text | JSON_EACH_TEXT | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEachText` |
| json_exists | JSON_EXISTS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonExists` |
| json_object_agg | JSON_OBJECT_AGG | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectAgg` |
| json_object_keys | JSON_OBJECT_KEYS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectKeys` |
| json_query | JSON_QUERY | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonQuery` |
| json_scalar | JSON_SCALAR | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonScalar` |
| json_serialize | JSON_SERIALIZE | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonSerialize` |
| json_strip_nulls | JSON_STRIP_NULLS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonStripNulls` |
| json_typeof | JSON_TYPEOF | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonTypeof` |
| json_value | JSON_VALUE | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonValue` |
| jsonb_array_elements | JSONB_ARRAY_ELEMENTS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbArrayElements` |
| jsonb_agg | JSONB_AGG | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbAgg` |
| jsonb_array_elements_text | JSONB_ARRAY_ELEMENTS_TEXT | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbArrayElementsText` |
Expand Down
21 changes: 19 additions & 2 deletions docs/INTEGRATING-WITH-DOCTRINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,26 @@ use Doctrine\ORM\Configuration;
$configuration = new Configuration();

# Register json functions
$configuration->addCustomStringFunction('JSON_AGG', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonAgg::class);
$configuration->addCustomStringFunction('JSON_ARRAY_LENGTH', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonArrayLength::class);
$configuration->addCustomStringFunction('JSON_BUILD_OBJECT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonBuildObject::class);
$configuration->addCustomStringFunction('JSONB_BUILD_OBJECT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbBuildObject::class);

$configuration->addCustomStringFunction('JSON_EACH', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEach::class);
$configuration->addCustomStringFunction('JSON_EACH_TEXT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEachText::class);
$configuration->addCustomStringFunction('JSON_EXISTS', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonExists::class);
$configuration->addCustomStringFunction('JSON_GET_FIELD', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField::class);
$configuration->addCustomStringFunction('JSON_GET_FIELD_AS_TEXT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsText::class);
$configuration->addCustomStringFunction('JSON_GET_FIELD_AS_INTEGER', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsInteger::class);
$configuration->addCustomStringFunction('JSON_GET_OBJECT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObject::class);
$configuration->addCustomStringFunction('JSON_GET_OBJECT_AS_TEXT', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObjectAsText::class);
$configuration->addCustomStringFunction('JSON_OBJECT_AGG', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectAgg::class);
$configuration->addCustomStringFunction('JSON_OBJECT_KEYS', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectKeys::class);
$configuration->addCustomStringFunction('JSON_QUERY', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonQuery::class);
$configuration->addCustomStringFunction('JSON_SCALAR', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonScalar::class);
$configuration->addCustomStringFunction('JSON_SERIALIZE', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonSerialize::class);
$configuration->addCustomStringFunction('JSON_STRIP_NULLS', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonStripNulls::class);
$configuration->addCustomStringFunction('JSON_VALUE', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonValue::class);
$configuration->addCustomStringFunction('TO_JSON', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToJson::class);
$configuration->addCustomStringFunction('ROW_TO_JSON', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RowToJson::class);
# Register text search functions
$configuration->addCustomStringFunction('TO_TSQUERY', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToTsquery::class);
$configuration->addCustomStringFunction('TO_TSVECTOR', MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToTsvector::class);
Expand Down
5 changes: 5 additions & 0 deletions docs/INTEGRATING-WITH-LARAVEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,19 @@ return [
'JSON_BUILD_OBJECT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonBuildObject::class,
'JSON_EACH' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEach::class,
'JSON_EACH_TEXT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEachText::class,
'JSON_EXISTS' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonExists::class,
'JSON_GET_FIELD' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField::class,
'JSON_GET_FIELD_AS_TEXT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsText::class,
'JSON_GET_FIELD_AS_INTEGER' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsInteger::class,
'JSON_GET_OBJECT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObject::class,
'JSON_GET_OBJECT_AS_TEXT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObjectAsText::class,
'JSON_OBJECT_AGG' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectAgg::class,
'JSON_OBJECT_KEYS' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectKeys::class,
'JSON_QUERY' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonQuery::class,
'JSON_SCALAR' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonScalar::class,
'JSON_SERIALIZE' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonSerialize::class,
'JSON_STRIP_NULLS' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonStripNulls::class,
'JSON_VALUE' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonValue::class,
'TO_JSON' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToJson::class,
'ROW_TO_JSON' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RowToJson::class,

Expand Down
7 changes: 6 additions & 1 deletion docs/INTEGRATING-WITH-SYMFONY.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,22 @@ doctrine:
JSON_BUILD_OBJECT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonBuildObject
JSON_EACH: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEach
JSON_EACH_TEXT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonEachText
JSON_EXISTS: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonExists
JSON_GET_FIELD: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField
JSON_GET_FIELD_AS_INTEGER: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsInteger
JSON_GET_FIELD_AS_TEXT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetFieldAsText
JSON_GET_OBJECT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObject
JSON_GET_OBJECT_AS_TEXT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetObjectAsText
JSON_OBJECT_AGG: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectAgg
JSON_OBJECT_KEYS: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonObjectKeys
JSON_QUERY: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonQuery
JSON_SCALAR: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonScalar
JSON_SERIALIZE: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonSerialize
JSON_STRIP_NULLS: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonStripNulls
JSON_TYPEOF: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonTypeof
JSON_VALUE: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonValue
TO_JSON: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ToJson
ROW_TO_JSON: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RowToJson
JSON_TYPEOF: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonTypeof

# jsonb specific functions
JSONB_AGG: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonbAgg
Expand Down
32 changes: 32 additions & 0 deletions src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonExists.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

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

/**
* Implementation of PostgreSQL JSON_EXISTS().
*
* Supports basic form:
* - json_exists(target_json, path_text)
*
* Note: The following variations are NOT supported due to DQL limitations:
* - PASSING variables
* - ON ERROR clause
* - ON EMPTY clause
* - WITH UNIQUE KEYS clause
*
* @see https://www.postgresql.org/docs/17/functions-json.html
* @since 2.10
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
class JsonExists extends BaseFunction
{
protected function customizeFunction(): void
{
$this->setFunctionPrototype('json_exists(%s, %s)');
$this->addNodeMapping('StringPrimary');
$this->addNodeMapping('StringPrimary');
}
}
37 changes: 37 additions & 0 deletions src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonQuery.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

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

/**
* Implementation of PostgreSQL JSON_QUERY().
*
* Supports basic form:
* - json_query(target_json, path_text)
*
* Note: The following variations are NOT supported due to DQL limitations:
* - PASSING variables
* - RETURNING type
* - WRAPPER clause
* - ON ERROR clause
* - ON EMPTY clause
* - WITH WRAPPER clause
* - WITH CONDITIONAL WRAPPER clause
* - EMPTY/NULL ON EMPTY
* - ERROR/NULL ON ERROR
*
* @see https://www.postgresql.org/docs/17/functions-json.html
* @since 2.10
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
class JsonQuery extends BaseFunction
{
protected function customizeFunction(): void
{
$this->setFunctionPrototype('json_query(%s, %s)');
$this->addNodeMapping('StringPrimary');
$this->addNodeMapping('StringPrimary');
}
}
30 changes: 30 additions & 0 deletions src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonScalar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

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

/**
* Implementation of PostgreSQL JSON_SCALAR().
*
* Supports basic form:
* - json_scalar(target_json)
*
* Note: The following variations are NOT supported due to DQL limitations:
* - RETURNING type
* - ON ERROR clause
* - ERROR/NULL ON ERROR
*
* @see https://www.postgresql.org/docs/17/functions-json.html
* @since 2.10
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
class JsonScalar extends BaseFunction
{
protected function customizeFunction(): void
{
$this->setFunctionPrototype('json_scalar(%s)');
$this->addNodeMapping('StringPrimary');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

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

/**
* Implementation of PostgreSQL JSON_SERIALIZE().
*
* Supports basic form:
* - json_serialize(expression)
*
* Note: The following variations are NOT supported due to DQL limitations:
* - RETURNING type
* - FORMAT JSON clause
* - ENCODING clause
*
* @see https://www.postgresql.org/docs/17/functions-json.html
* @since 2.10
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
class JsonSerialize extends BaseFunction
{
protected function customizeFunction(): void
{
$this->setFunctionPrototype('json_serialize(%s)');
$this->addNodeMapping('StringPrimary');
}
}
35 changes: 35 additions & 0 deletions src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonValue.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

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

/**
* Implementation of PostgreSQL JSON_VALUE().
*
* Supports basic form:
* - json_value(target_json, path_text)
*
* Note: The following variations are NOT supported due to DQL limitations:
* - PASSING variables
* - RETURNING type
* - DEFAULT clause
* - ON ERROR clause
* - ON EMPTY clause
* - ERROR/NULL/DEFAULT ON ERROR
* - ERROR/NULL/DEFAULT ON EMPTY
*
* @see https://www.postgresql.org/docs/17/functions-json.html
* @since 2.10
*
* @author Martin Georgiev <martin.georgiev@gmail.com>
*/
class JsonValue extends BaseFunction
{
protected function customizeFunction(): void
{
$this->setFunctionPrototype('json_value(%s, %s)');
$this->addNodeMapping('StringPrimary');
$this->addNodeMapping('StringPrimary');
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?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\JsonExists;

class JsonExistsTest extends TestCase
{
protected function getStringFunctions(): array
{
return [
'JSON_EXISTS' => JsonExists::class,
];
}

protected function getExpectedSqlStatements(): array
{
return [
// Basic usage
"SELECT json_exists(c0_.object1, '$.name') AS sclr_0 FROM ContainsJsons c0_",
// Nested path
"SELECT json_exists(c0_.object1, '$.address.city') AS sclr_0 FROM ContainsJsons c0_",
// Array element
"SELECT json_exists(c0_.object1, '$.items[0]') AS sclr_0 FROM ContainsJsons c0_",
];
}

protected function getDqlStatements(): array
{
return [
\sprintf("SELECT JSON_EXISTS(e.object1, '$.name') FROM %s e", ContainsJsons::class),
\sprintf("SELECT JSON_EXISTS(e.object1, '$.address.city') FROM %s e", ContainsJsons::class),
\sprintf("SELECT JSON_EXISTS(e.object1, '$.items[0]') FROM %s e", ContainsJsons::class),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?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\JsonQuery;

class JsonQueryTest extends TestCase
{
protected function getStringFunctions(): array
{
return [
'JSON_QUERY' => JsonQuery::class,
];
}

protected function getExpectedSqlStatements(): array
{
return [
"SELECT json_query(c0_.object1, '$.items[*]') AS sclr_0 FROM ContainsJsons c0_",
"SELECT json_query(c0_.object1, '$.address') AS sclr_0 FROM ContainsJsons c0_",
// Additional test cases for important scenarios
"SELECT json_query(c0_.object1, '$.store.book[*].author') AS sclr_0 FROM ContainsJsons c0_",
"SELECT json_query(c0_.object1, '$.store.book[0 to 2]') AS sclr_0 FROM ContainsJsons c0_",
"SELECT json_query(c0_.object1, '$.store.book[*]?(@.price > 10)') AS sclr_0 FROM ContainsJsons c0_",
];
}

protected function getDqlStatements(): array
{
return [
\sprintf("SELECT JSON_QUERY(e.object1, '$.items[*]') FROM %s e", ContainsJsons::class),
\sprintf("SELECT JSON_QUERY(e.object1, '$.address') FROM %s e", ContainsJsons::class),
// Additional test cases matching the SQL statements above
\sprintf("SELECT JSON_QUERY(e.object1, '$.store.book[*].author') FROM %s e", ContainsJsons::class),
\sprintf("SELECT JSON_QUERY(e.object1, '$.store.book[0 to 2]') FROM %s e", ContainsJsons::class),
\sprintf("SELECT JSON_QUERY(e.object1, '$.store.book[*]?(@.price > 10)') FROM %s e", ContainsJsons::class),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?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\JsonScalar;

class JsonScalarTest extends TestCase
{
protected function getStringFunctions(): array
{
return [
'JSON_SCALAR' => JsonScalar::class,
];
}

protected function getExpectedSqlStatements(): array
{
return [
'SELECT json_scalar(c0_.object1) AS sclr_0 FROM ContainsJsons c0_',
];
}

protected function getDqlStatements(): array
{
return [
\sprintf('SELECT JSON_SCALAR(e.object1) FROM %s e', ContainsJsons::class),
];
}
}
Loading