diff --git a/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md b/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md index 2d638bc3..946d25ac 100644 --- a/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md +++ b/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md @@ -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` | diff --git a/docs/INTEGRATING-WITH-DOCTRINE.md b/docs/INTEGRATING-WITH-DOCTRINE.md index f27ee4da..d9a6611a 100644 --- a/docs/INTEGRATING-WITH-DOCTRINE.md +++ b/docs/INTEGRATING-WITH-DOCTRINE.md @@ -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); diff --git a/docs/INTEGRATING-WITH-LARAVEL.md b/docs/INTEGRATING-WITH-LARAVEL.md index 7664fdb5..e8192c78 100644 --- a/docs/INTEGRATING-WITH-LARAVEL.md +++ b/docs/INTEGRATING-WITH-LARAVEL.md @@ -111,6 +111,7 @@ 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, @@ -118,7 +119,11 @@ return [ '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, diff --git a/docs/INTEGRATING-WITH-SYMFONY.md b/docs/INTEGRATING-WITH-SYMFONY.md index 26c528c4..20016ce3 100644 --- a/docs/INTEGRATING-WITH-SYMFONY.md +++ b/docs/INTEGRATING-WITH-SYMFONY.md @@ -104,6 +104,7 @@ 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 @@ -111,10 +112,14 @@ doctrine: 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 diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonExists.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonExists.php new file mode 100644 index 00000000..d07991b5 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonExists.php @@ -0,0 +1,32 @@ + + */ +class JsonExists extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('json_exists(%s, %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonQuery.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonQuery.php new file mode 100644 index 00000000..552de27a --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonQuery.php @@ -0,0 +1,37 @@ + + */ +class JsonQuery extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('json_query(%s, %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonScalar.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonScalar.php new file mode 100644 index 00000000..4b4189d2 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonScalar.php @@ -0,0 +1,30 @@ + + */ +class JsonScalar extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('json_scalar(%s)'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonSerialize.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonSerialize.php new file mode 100644 index 00000000..94951da2 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonSerialize.php @@ -0,0 +1,30 @@ + + */ +class JsonSerialize extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('json_serialize(%s)'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonValue.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonValue.php new file mode 100644 index 00000000..f977212c --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonValue.php @@ -0,0 +1,35 @@ + + */ +class JsonValue extends BaseFunction +{ + protected function customizeFunction(): void + { + $this->setFunctionPrototype('json_value(%s, %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonExistsTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonExistsTest.php new file mode 100644 index 00000000..2467d59e --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonExistsTest.php @@ -0,0 +1,39 @@ + 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), + ]; + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonQueryTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonQueryTest.php new file mode 100644 index 00000000..5dc81ee4 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonQueryTest.php @@ -0,0 +1,42 @@ + 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), + ]; + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonScalarTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonScalarTest.php new file mode 100644 index 00000000..b58f8792 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonScalarTest.php @@ -0,0 +1,32 @@ + 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), + ]; + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonSerializeTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonSerializeTest.php new file mode 100644 index 00000000..44c99748 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonSerializeTest.php @@ -0,0 +1,39 @@ + JsonSerialize::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + // Basic serialization + 'SELECT json_serialize(c0_.object1) AS sclr_0 FROM ContainsJsons c0_', + // With expression + 'SELECT json_serialize(UPPER(c0_.object1)) AS sclr_0 FROM ContainsJsons c0_', + // With literal + "SELECT json_serialize('{\"key\": \"value\"}') AS sclr_0 FROM ContainsJsons c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + \sprintf('SELECT JSON_SERIALIZE(e.object1) FROM %s e', ContainsJsons::class), + \sprintf('SELECT JSON_SERIALIZE(UPPER(e.object1)) FROM %s e', ContainsJsons::class), + \sprintf("SELECT JSON_SERIALIZE('{\"key\": \"value\"}') FROM %s e", ContainsJsons::class), + ]; + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonValueTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonValueTest.php new file mode 100644 index 00000000..1a809a89 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/JsonValueTest.php @@ -0,0 +1,36 @@ + JsonValue::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + "SELECT json_value(c0_.object1, '$.name') AS sclr_0 FROM ContainsJsons c0_", + "SELECT json_value(c0_.object1, '$.address.city') AS sclr_0 FROM ContainsJsons c0_", + "SELECT json_value(c0_.object1, '$.items[0]') AS sclr_0 FROM ContainsJsons c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + \sprintf("SELECT JSON_VALUE(e.object1, '$.name') FROM %s e", ContainsJsons::class), + \sprintf("SELECT JSON_VALUE(e.object1, '$.address.city') FROM %s e", ContainsJsons::class), + \sprintf("SELECT JSON_VALUE(e.object1, '$.items[0]') FROM %s e", ContainsJsons::class), + ]; + } +}