diff --git a/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md b/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md index 12076e78..b9fcff0a 100644 --- a/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md +++ b/docs/AVAILABLE-FUNCTIONS-AND-OPERATORS.md @@ -1,22 +1,27 @@ # Available operators -| PostgreSQL operator | Register for DQL as | Implemented by -|---------------------|-------------------------------------------------|-----------------------------------------------------------------------| -| @> | CONTAINS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Contains` | -| <@ | IS_CONTAINED_BY | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IsContainedBy` | -| && | OVERLAPS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Overlaps` | -| -> | JSON_GET_FIELD | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField` | -| ->> | 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` | -| ilike | ILIKE ([Usage note](USE-CASES-AND-EXAMPLES.md)) | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Ilike` | -| similar to | SIMILAR_TO | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\SimilarTo` | -| not similar to | NOT_SIMILAR_TO | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\NotSimilarTo` | -| ~ | REGEXP | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Regexp` | -| ~* | IREGEXP | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IRegexp` | -| !~ | NOT_REGEXP | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\NotRegexp` | -| !~* | NOT_IREGEXP | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\NotIRegexp` | -| @@ | TSMATCH | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Tsmatch` | +| PostgreSQL operator | Register for DQL as | Implemented by +|---|---|---| +| @> | CONTAINS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Contains` | +| <@ | IS_CONTAINED_BY | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IsContainedBy` | +| && | OVERLAPS | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Overlaps` | +| ? | RIGHT_EXISTS_ON_LEFT | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\TheRightExistsOnTheLeft` | +| ?& | ALL_ON_RIGHT_EXIST_ON_LEFT | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\AllOnTheRightExistOnTheLeft` | +| ?\| | ANY_ON_RIGHT_EXISTS_ON_LEFT | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\AnyOnTheRightExistsOnTheLeft` | +| @? | RETURNS_VALUE_FOR_JSON_VALUE | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ReturnsValueForJsonValue` | +| #- | DELETE_AT_PATH | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\DeleteAtPath` | +| -> | JSON_GET_FIELD | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonGetField` | +| ->> | 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` | +| ilike | ILIKE ([Usage note](USE-CASES-AND-EXAMPLES.md)) | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Ilike` | +| similar to | SIMILAR_TO | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\SimilarTo` | +| not similar to | NOT_SIMILAR_TO | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\NotSimilarTo` | +| ~ | REGEXP | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Regexp` | +| ~* | IREGEXP | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IRegexp` | +| !~ | NOT_REGEXP | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\NotRegexp` | +| !~* | NOT_IREGEXP | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\NotIRegexp` | +| @@ | TSMATCH | `MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Tsmatch` | # Available functions diff --git a/docs/INTEGRATING-WITH-LARAVEL.md b/docs/INTEGRATING-WITH-LARAVEL.md index 2b182867..3b5c63de 100644 --- a/docs/INTEGRATING-WITH-LARAVEL.md +++ b/docs/INTEGRATING-WITH-LARAVEL.md @@ -73,11 +73,16 @@ return [ 'ANY_OF' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Any::class, # operators for working with array and json(b) data - 'CONTAINS' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Contains::class, - 'IS_CONTAINED_BY' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IsContainedBy::class, - 'OVERLAPS' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Overlaps::class, 'GREATEST' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Greatest::class, 'LEAST' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Least::class, + 'CONTAINS' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Contains::class, // @> + 'IS_CONTAINED_BY' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IsContainedBy::class, // <@ + 'OVERLAPS' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Overlaps::class, // && + 'RIGHT_EXISTS_ON_LEFT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\TheRightExistsOnTheLeft::class, // ? + 'ALL_ON_RIGHT_EXIST_ON_LEFT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\AllOnTheRightExistOnTheLeft::class, // ?& + 'ANY_ON_RIGHT_EXISTS_ON_LEFT' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\AnyOnTheRightExistsOnTheLeft::class, // ?| + 'RETURNS_VALUE_FOR_JSON_VALUE' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ReturnsValueForJsonValue::class, // @? + 'DELETE_AT_PATH' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\DeleteAtPath::class, // #- # array and string specific functions 'IN_ARRAY' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\InArray::class, @@ -94,10 +99,10 @@ return [ 'ARRAY_REPLACE' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayReplace::class, 'ARRAY_TO_JSON' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayToJson::class, 'ARRAY_TO_STRING' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayToString::class, + 'STARTS_WITH' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StartsWith::class, 'STRING_AGG' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StringAgg::class, 'STRING_TO_ARRAY' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StringToArray::class, 'UNNEST' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Unnest::class, - 'STARTS_WITH' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StartsWith::class, # json specific functions 'JSON_AGG' => MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonAgg::class, diff --git a/docs/INTEGRATING-WITH-SYMFONY.md b/docs/INTEGRATING-WITH-SYMFONY.md index 679e8249..f4e17b3a 100644 --- a/docs/INTEGRATING-WITH-SYMFONY.md +++ b/docs/INTEGRATING-WITH-SYMFONY.md @@ -66,12 +66,17 @@ doctrine: ANY_OF: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Any # operators for working with array and json(b) data - CONTAINS: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Contains - IS_CONTAINED_BY: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IsContainedBy - OVERLAPS: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Overlaps GREATEST: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Greatest LEAST: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Least - + CONTAINS: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Contains # @> + IS_CONTAINED_BY: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\IsContainedBy # <@ + OVERLAPS: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Overlaps # && + RIGHT_EXISTS_ON_LEFT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\TheRightExistsOnTheLeft # ? + ALL_ON_RIGHT_EXIST_ON_LEFT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\AllOnTheRightExistOnTheLeft # ?& + ANY_ON_RIGHT_EXISTS_ON_LEFT: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\AnyOnTheRightExistsOnTheLeft # ?| + RETURNS_VALUE_FOR_JSON_VALUE: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ReturnsValueForJsonValue # @? + DELETE_AT_PATH: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\DeleteAtPath # #- + # array and string specific functions IN_ARRAY: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\InArray ARRAY: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Arr @@ -87,10 +92,10 @@ doctrine: ARRAY_REPLACE: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayReplace ARRAY_TO_JSON: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayToJson ARRAY_TO_STRING: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\ArrayToString + STARTS_WITH: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StartsWith STRING_AGG: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StringAgg STRING_TO_ARRAY: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StringToArray UNNEST: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Unnest - STARTS_WITH: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\StartsWith # json specific functions JSON_AGG: MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\JsonAgg diff --git a/docs/USE-CASES-AND-EXAMPLES.md b/docs/USE-CASES-AND-EXAMPLES.md index c69a3306..aebbc6cb 100644 --- a/docs/USE-CASES-AND-EXAMPLES.md +++ b/docs/USE-CASES-AND-EXAMPLES.md @@ -1,7 +1,7 @@ Clarification on usage of `ILIKE`, `CONTAINS`, `IS_CONTAINED_BY`, `DATE_OVERLAPS` and some other operator-like functions --- -`Error: Expected =, <, <=, <>, >, >=, !=, got 'ILIKE'"` (or similar) is probably one of the most common DQL errors you may experience when working with this library. The cause for is that when parsing the DQL Doctrine won't recognise `ILIKE` as a known operator. In fact `ILIKE` is registered as a function. +`Error: Expected =, <, <=, <>, >, >=, !=, got 'ILIKE'"` (or similar) is probably one of the most common DQL errors you may experience when working with this library. The cause for is that when parsing the DQL Doctrine won't recognise `ILIKE` as a known operator. In fact `ILIKE` is registered as a boolean function. Doctrine doesn't provide easy support for implementing custom operators. This may change in the future but for now it is easier to trick the DQL parser with a boolean expression. Example intent with DQL: diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AllOnTheRightExistOnTheLeft.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AllOnTheRightExistOnTheLeft.php new file mode 100644 index 00000000..4ae0fff7 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AllOnTheRightExistOnTheLeft.php @@ -0,0 +1,23 @@ + + */ +class AllOnTheRightExistOnTheLeft extends BaseFunction +{ + protected function customiseFunction(): void + { + $this->setFunctionPrototype('(%s ??& %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AnyOnTheRightExistsOnTheLeft.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AnyOnTheRightExistsOnTheLeft.php new file mode 100644 index 00000000..93e952cb --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AnyOnTheRightExistsOnTheLeft.php @@ -0,0 +1,23 @@ + + */ +class AnyOnTheRightExistsOnTheLeft extends BaseFunction +{ + protected function customiseFunction(): void + { + $this->setFunctionPrototype('(%s ??| %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/DeleteAtPath.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/DeleteAtPath.php new file mode 100644 index 00000000..5959be7a --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/DeleteAtPath.php @@ -0,0 +1,23 @@ + + */ +class DeleteAtPath extends BaseFunction +{ + protected function customiseFunction(): void + { + $this->setFunctionPrototype('(%s #- %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ReturnsValueForJsonValue.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ReturnsValueForJsonValue.php new file mode 100644 index 00000000..a8438ab8 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ReturnsValueForJsonValue.php @@ -0,0 +1,23 @@ + + */ +class ReturnsValueForJsonValue extends BaseFunction +{ + protected function customiseFunction(): void + { + $this->setFunctionPrototype('(%s @?? %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TheRightExistsOnTheLeft.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TheRightExistsOnTheLeft.php new file mode 100644 index 00000000..98cfab2b --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TheRightExistsOnTheLeft.php @@ -0,0 +1,23 @@ + + */ +class TheRightExistsOnTheLeft extends BaseFunction +{ + protected function customiseFunction(): void + { + $this->setFunctionPrototype('(%s ?? %s)'); + $this->addNodeMapping('StringPrimary'); + $this->addNodeMapping('StringPrimary'); + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AllOnTheRightExistOnTheLeftTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AllOnTheRightExistOnTheLeftTest.php new file mode 100644 index 00000000..3f41c6a0 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AllOnTheRightExistOnTheLeftTest.php @@ -0,0 +1,34 @@ + AllOnTheRightExistOnTheLeft::class, + 'ARRAY' => Arr::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + "SELECT (c0_.object1 ??& ARRAY['test']) AS sclr_0 FROM ContainsJsons c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + \sprintf("SELECT ALL_ON_RIGHT_EXIST_ON_LEFT(e.object1, ARRAY('test')) FROM %s e", ContainsJsons::class), + ]; + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AnyOnTheRightExistsOnTheLeftTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AnyOnTheRightExistsOnTheLeftTest.php new file mode 100644 index 00000000..c89aa06f --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/AnyOnTheRightExistsOnTheLeftTest.php @@ -0,0 +1,34 @@ + AnyOnTheRightExistsOnTheLeft::class, + 'ARRAY' => Arr::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + "SELECT (c0_.object1 ??| ARRAY['test']) AS sclr_0 FROM ContainsJsons c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + \sprintf("SELECT ANY_ON_RIGHT_EXISTS_ON_LEFT(e.object1, ARRAY('test')) FROM %s e", ContainsJsons::class), + ]; + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/DeleteAtPathTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/DeleteAtPathTest.php new file mode 100644 index 00000000..e0c6fee1 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/DeleteAtPathTest.php @@ -0,0 +1,32 @@ + DeleteAtPath::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'SELECT (c0_.object1 #- c0_.object2) AS sclr_0 FROM ContainsJsons c0_', + ]; + } + + protected function getDqlStatements(): array + { + return [ + \sprintf('SELECT DELETE_AT_PATH(e.object1, e.object2) FROM %s e', ContainsJsons::class), + ]; + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ReturnsValueForJsonValueTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ReturnsValueForJsonValueTest.php new file mode 100644 index 00000000..3bccce92 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/ReturnsValueForJsonValueTest.php @@ -0,0 +1,32 @@ + ReturnsValueForJsonValue::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + "SELECT (c0_.object1 @?? '$.test[*] ?? (@ > 2)') AS sclr_0 FROM ContainsJsons c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + \sprintf("SELECT RETURNS_VALUE_FOR_JSON_VALUE(e.object1, '$.test[*] ?? (@ > 2)') FROM %s e", ContainsJsons::class), + ]; + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TheRightExistsOnTheLeftTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TheRightExistsOnTheLeftTest.php new file mode 100644 index 00000000..a9f0cd71 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/TheRightExistsOnTheLeftTest.php @@ -0,0 +1,34 @@ + TheRightExistsOnTheLeft::class, + 'ARRAY' => Arr::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + "SELECT (c0_.object1 ?? ARRAY['test']) AS sclr_0 FROM ContainsJsons c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + \sprintf("SELECT RIGHT_EXISTS_ON_LEFT(e.object1, ARRAY('test')) FROM %s e", ContainsJsons::class), + ]; + } +}