diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseRegexpFunction.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseRegexpFunction.php deleted file mode 100644 index 5676376c..00000000 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseRegexpFunction.php +++ /dev/null @@ -1,22 +0,0 @@ -getParameterCount() - 1); - $this->setFunctionPrototype($this->getFunctionName().'(%s'.$parameters.')'); - - for ($i = 0; $i < $this->getParameterCount(); $i++) { - $this->addNodeMapping('StringPrimary'); - } - } -} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpLike.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpLike.php index b8bcba74..65b8b793 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpLike.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpLike.php @@ -5,8 +5,9 @@ namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; /** - * Implementation of PostgreSQL REGEXP_LIKE() with flags. + * @deprecated This function will be dropped in v4.0. Use RegexpLike instead. * + * Implementation of PostgreSQL REGEXP_LIKE() with flags. * @see https://www.postgresql.org/docs/15/functions-matching.html#FUNCTIONS-POSIX-REGEXP * @see https://www.postgresql.org/docs/15/functions-matching.html#POSIX-EMBEDDED-OPTIONS-TABLE * @since 2.0 @@ -17,7 +18,7 @@ class FlaggedRegexpLike extends BaseFunction { protected function customizeFunction(): void { - $this->setFunctionPrototype('regexp_like(%s, %s, 1, %s)'); + $this->setFunctionPrototype('regexp_like(%s, %s, %s)'); $this->addNodeMapping('StringPrimary'); $this->addNodeMapping('StringPrimary'); $this->addNodeMapping('StringPrimary'); diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpMatch.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpMatch.php index 2132dd59..27c49ad7 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpMatch.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpMatch.php @@ -5,8 +5,9 @@ namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; /** - * Implementation of PostgreSQL REGEXP_MATCH() with flags. + * @deprecated This function will be dropped in v4.0. Use RegexpMatch instead. * + * Implementation of PostgreSQL REGEXP_MATCH() with flags. * @see https://www.postgresql.org/docs/15/functions-matching.html#FUNCTIONS-POSIX-REGEXP * @see https://www.postgresql.org/docs/15/functions-matching.html#POSIX-EMBEDDED-OPTIONS-TABLE * @since 2.0 diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpReplace.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpReplace.php index ec4f99ca..d79edc9f 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpReplace.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpReplace.php @@ -5,8 +5,9 @@ namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; /** - * Implementation of PostgreSQL REGEXP_REPLACE(). + * @deprecated This function will be dropped in v4.0. Use RegexpReplace instead. * + * Implementation of PostgreSQL REGEXP_REPLACE(). * @see https://www.postgresql.org/docs/15/functions-matching.html#FUNCTIONS-POSIX-REGEXP * @since 2.5 * diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpLike.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpLike.php index 42fa3fb4..c928967c 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpLike.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpLike.php @@ -7,20 +7,36 @@ /** * Implementation of PostgreSQL REGEXP_LIKE(). * - * @see https://www.postgresql.org/docs/15/functions-matching.html#FUNCTIONS-POSIX-REGEXP + * Returns true if a string matches a POSIX regular expression pattern, or false if it does not. + * + * @see https://www.postgresql.org/docs/17/functions-matching.html#FUNCTIONS-POSIX-REGEXP * @since 2.0 * * @author Martin Georgiev + * + * @example Using it in DQL: "SELECT REGEXP_LIKE(e.text, 'pattern', 'i') FROM Entity e" */ -class RegexpLike extends BaseRegexpFunction +class RegexpLike extends BaseVariadicFunction { + protected function getNodeMappingPattern(): array + { + return [ + 'StringPrimary', + ]; + } + protected function getFunctionName(): string { return 'regexp_like'; } - protected function getParameterCount(): int + protected function getMinArgumentCount(): int { return 2; } + + protected function getMaxArgumentCount(): int + { + return 3; + } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpMatch.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpMatch.php index 22bbe46a..889dc0fc 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpMatch.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpMatch.php @@ -7,20 +7,36 @@ /** * Implementation of PostgreSQL REGEXP_MATCH(). * - * @see https://www.postgresql.org/docs/15/functions-matching.html#FUNCTIONS-POSIX-REGEXP + * Returns the first substring(s) that match a POSIX regular expression pattern, or NULL if there is no match. + * + * @see https://www.postgresql.org/docs/17/functions-matching.html#FUNCTIONS-POSIX-REGEXP * @since 2.0 * * @author Martin Georgiev + * + * @example Using it in DQL: "SELECT REGEXP_MATCH(e.text, 'pattern', 'i') FROM Entity e" */ -class RegexpMatch extends BaseRegexpFunction +class RegexpMatch extends BaseVariadicFunction { + protected function getNodeMappingPattern(): array + { + return [ + 'StringPrimary', + ]; + } + protected function getFunctionName(): string { return 'regexp_match'; } - protected function getParameterCount(): int + protected function getMinArgumentCount(): int { return 2; } + + protected function getMaxArgumentCount(): int + { + return 3; + } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php index 2a426fb2..288d406a 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php @@ -13,6 +13,7 @@ * @since 2.5 * * @author Colin Doig + * @author Martin Georgiev * * @example Using it in DQL: "SELECT REGEXP_REPLACE(e.text, 'pattern', 'replacement', 3, 2, 'i') FROM Entity e" */ @@ -20,13 +21,17 @@ class RegexpReplace extends BaseVariadicFunction { protected function getNodeMappingPattern(): array { + /* + * PostgreSQL overloads the 4th argument depending on its type: + * - if the 4th arg is a string, it’s taken as flags. + * - if the 4th arg is an integer, it’s taken as start position. This can be extended with the Nth argument. + */ return [ - 'StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary', - 'StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary', - 'StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary,StringPrimary', - 'StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary', - 'StringPrimary,StringPrimary,StringPrimary,StringPrimary', - 'StringPrimary,StringPrimary,StringPrimary', + 'StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary', // with start, N and flags: regexp_replace(string, pattern, replacement, 3, 2, 'i') + 'StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary', // with start and N: regexp_replace(string, pattern, replacement, 3, 2) + 'StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary', // with start: regexp_replace(string, pattern, replacement, 3) + 'StringPrimary,StringPrimary,StringPrimary,StringPrimary', // with flags: regexp_replace(string, pattern, replacement, 'i') + 'StringPrimary,StringPrimary,StringPrimary', // basic replacement: regexp_replace(string, pattern, replacement) ]; } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseVariadicFunctionTestCase.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseVariadicFunctionTestCase.php index d262b1ff..511ec76f 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseVariadicFunctionTestCase.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/BaseVariadicFunctionTestCase.php @@ -28,7 +28,7 @@ public function throws_an_exception_when_lexer_is_not_populated_with_a_lookahead ->willReturn(new Configuration()); $query = new Query($em); - $query->setDQL('TRUE'); + $query->setDQL('SELECT 1'); $parser = new Parser($query); $parser->getLexer()->moveNext(); diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpLikeTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpLikeTest.php index 9264fadc..0504f973 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpLikeTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpLikeTest.php @@ -12,14 +12,14 @@ class FlaggedRegexpLikeTest extends TestCase protected function getStringFunctions(): array { return [ - 'FLAGGED_REGEXP_LIKE' => FlaggedRegexpLike::class, + 'FLAGGED_REGEXP_LIKE' => FlaggedRegexpLike::class, // @phpstan-ignore-line ]; } protected function getExpectedSqlStatements(): array { return [ - "SELECT regexp_like(c0_.text1, 'pattern', 1, 'i') AS sclr_0 FROM ContainsTexts c0_", + "SELECT regexp_like(c0_.text1, 'pattern', 'i') AS sclr_0 FROM ContainsTexts c0_", ]; } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpMatchTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpMatchTest.php index 5b099e2b..bda8ede4 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpMatchTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpMatchTest.php @@ -12,7 +12,7 @@ class FlaggedRegexpMatchTest extends TestCase protected function getStringFunctions(): array { return [ - 'FLAGGED_REGEXP_MATCH' => FlaggedRegexpMatch::class, + 'FLAGGED_REGEXP_MATCH' => FlaggedRegexpMatch::class, // @phpstan-ignore-line ]; } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpReplaceTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpReplaceTest.php index 76c1b10e..5e7dad3a 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpReplaceTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/FlaggedRegexpReplaceTest.php @@ -12,7 +12,7 @@ class FlaggedRegexpReplaceTest extends TestCase protected function getStringFunctions(): array { return [ - 'FLAGGED_REGEXP_REPLACE' => FlaggedRegexpReplace::class, + 'FLAGGED_REGEXP_REPLACE' => FlaggedRegexpReplace::class, // @phpstan-ignore-line ]; } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpLikeTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpLikeTest.php index c525f344..01e8b8c0 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpLikeTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpLikeTest.php @@ -5,10 +5,17 @@ namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsTexts; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseVariadicFunction; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RegexpLike; -class RegexpLikeTest extends TestCase +class RegexpLikeTest extends BaseVariadicFunctionTestCase { + protected function createFixture(): BaseVariadicFunction + { + return new RegexpLike('REGEXP_LIKE'); + } + protected function getStringFunctions(): array { return [ @@ -19,14 +26,34 @@ protected function getStringFunctions(): array protected function getExpectedSqlStatements(): array { return [ - "SELECT regexp_like(c0_.text1, 'pattern') AS sclr_0 FROM ContainsTexts c0_", + 'basic match' => "SELECT regexp_like(c0_.text1, 'pattern') AS sclr_0 FROM ContainsTexts c0_", + 'with flags' => "SELECT regexp_like(c0_.text1, 'pattern', 'i') AS sclr_0 FROM ContainsTexts c0_", ]; } protected function getDqlStatements(): array { return [ - \sprintf("SELECT REGEXP_LIKE(e.text1, 'pattern') FROM %s e", ContainsTexts::class), + 'basic match' => \sprintf("SELECT REGEXP_LIKE(e.text1, 'pattern') FROM %s e", ContainsTexts::class), + 'with flags' => \sprintf("SELECT REGEXP_LIKE(e.text1, 'pattern', 'i') FROM %s e", ContainsTexts::class), ]; } + + public function test_too_few_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('regexp_like() requires at least 2 arguments'); + + $dql = \sprintf('SELECT REGEXP_LIKE(e.text1) FROM %s e', ContainsTexts::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_many_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('regexp_like() requires between 2 and 3 arguments'); + + $dql = \sprintf("SELECT REGEXP_LIKE(e.text1, 'pattern', 'i', 'extra_arg') FROM %s e", ContainsTexts::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpMatchTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpMatchTest.php index fe8d1e57..303b9ea3 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpMatchTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpMatchTest.php @@ -5,10 +5,17 @@ namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions; use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsTexts; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseVariadicFunction; +use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException; use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RegexpMatch; -class RegexpMatchTest extends TestCase +class RegexpMatchTest extends BaseVariadicFunctionTestCase { + protected function createFixture(): BaseVariadicFunction + { + return new RegexpMatch('REGEXP_MATCH'); + } + protected function getStringFunctions(): array { return [ @@ -19,14 +26,34 @@ protected function getStringFunctions(): array protected function getExpectedSqlStatements(): array { return [ - "SELECT regexp_match(c0_.text1, 'pattern') AS sclr_0 FROM ContainsTexts c0_", + 'basic match' => "SELECT regexp_match(c0_.text1, 'pattern') AS sclr_0 FROM ContainsTexts c0_", + 'with flags' => "SELECT regexp_match(c0_.text1, 'pattern', 'i') AS sclr_0 FROM ContainsTexts c0_", ]; } protected function getDqlStatements(): array { return [ - \sprintf("SELECT REGEXP_MATCH(e.text1, 'pattern') FROM %s e", ContainsTexts::class), + 'basic match' => \sprintf("SELECT REGEXP_MATCH(e.text1, 'pattern') FROM %s e", ContainsTexts::class), + 'with flags' => \sprintf("SELECT REGEXP_MATCH(e.text1, 'pattern', 'i') FROM %s e", ContainsTexts::class), ]; } + + public function test_too_few_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('regexp_match() requires at least 2 arguments'); + + $dql = \sprintf('SELECT REGEXP_MATCH(e.text1) FROM %s e', ContainsTexts::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } + + public function test_too_many_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('regexp_match() requires between 2 and 3 arguments'); + + $dql = \sprintf("SELECT REGEXP_MATCH(e.text1, 'pattern', 'i', 'extra_arg') FROM %s e", ContainsTexts::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplaceTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplaceTest.php index 17ea390d..eae39219 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplaceTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplaceTest.php @@ -29,7 +29,6 @@ protected function getExpectedSqlStatements(): array 'basic replacement' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement') AS sclr_0 FROM ContainsTexts c0_", 'with flags but no start position' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 'i') AS sclr_0 FROM ContainsTexts c0_", 'with start position' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 3) AS sclr_0 FROM ContainsTexts c0_", - 'with start position and flags' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 3, 'i') AS sclr_0 FROM ContainsTexts c0_", 'with occurrence count but no flags' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 3, 2) AS sclr_0 FROM ContainsTexts c0_", 'with occurrence count and flags' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 3, 2, 'i') AS sclr_0 FROM ContainsTexts c0_", ]; @@ -41,7 +40,6 @@ protected function getDqlStatements(): array 'basic replacement' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement') FROM %s e", ContainsTexts::class), 'with flags but no start position' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 'i') FROM %s e", ContainsTexts::class), 'with start position' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 3) FROM %s e", ContainsTexts::class), - 'with start position and flags' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 3, 'i') FROM %s e", ContainsTexts::class), 'with occurrence count but no flags' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 3, 2) FROM %s e", ContainsTexts::class), 'with occurrence count and flags' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 3, 2, 'i') FROM %s e", ContainsTexts::class), ];