From 45e5358a397b288abba43b3ce3c8208efc5bf65e Mon Sep 17 00:00:00 2001 From: Martin Georgiev Date: Sun, 20 Apr 2025 11:57:19 +0100 Subject: [PATCH 1/2] feat: add support for `REGEXP_COUNT()`, `REGEXP_INSTR()` and `REGEXP_SUBSTR()` and extend support for `REGEXP_REPLACE()` --- .../ORM/Query/AST/Functions/RegexpCount.php | 43 ++++++++++++ .../ORM/Query/AST/Functions/RegexpInstr.php | 45 +++++++++++++ .../ORM/Query/AST/Functions/RegexpReplace.php | 19 +++++- .../ORM/Query/AST/Functions/RegexpSubstr.php | 46 +++++++++++++ .../Query/AST/Functions/RegexpCountTest.php | 65 ++++++++++++++++++ .../Query/AST/Functions/RegexpInstrTest.php | 67 +++++++++++++++++++ .../Query/AST/Functions/RegexpReplaceTest.php | 37 +++++++++- .../Query/AST/Functions/RegexpSubstrTest.php | 65 ++++++++++++++++++ 8 files changed, 382 insertions(+), 5 deletions(-) create mode 100644 src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCount.php create mode 100644 src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstr.php create mode 100644 src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstr.php create mode 100644 tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCountTest.php create mode 100644 tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstrTest.php create mode 100644 tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstrTest.php diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCount.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCount.php new file mode 100644 index 00000000..dc46da50 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCount.php @@ -0,0 +1,43 @@ + + * + * @example Using it in DQL: "SELECT REGEXP_COUNT(e.text, '\d\d\d') FROM Entity e" + */ +class RegexpCount extends BaseVariadicFunction +{ + protected function getNodeMappingPattern(): array + { + return [ + 'StringPrimary,StringPrimary,ArithmeticPrimary,StringPrimary', + 'StringPrimary,StringPrimary', + ]; + } + + protected function getFunctionName(): string + { + return 'regexp_count'; + } + + protected function getMinArgumentCount(): int + { + return 2; + } + + protected function getMaxArgumentCount(): int + { + return 4; + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstr.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstr.php new file mode 100644 index 00000000..2007e3c5 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstr.php @@ -0,0 +1,45 @@ + + * + * @example Using it in DQL: "SELECT REGEXP_INSTR(e.text, 'c(.)(..)', 1, 1, 0, 'i') FROM Entity e" + */ +class RegexpInstr extends BaseVariadicFunction +{ + protected function getNodeMappingPattern(): array + { + return [ + 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary', + 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,ArithmeticPrimary', + 'StringPrimary,StringPrimary', + ]; + } + + protected function getFunctionName(): string + { + return 'regexp_instr'; + } + + protected function getMinArgumentCount(): int + { + return 2; + } + + protected function getMaxArgumentCount(): int + { + return 6; + } +} diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php index 5276fb6a..a3beba29 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php @@ -12,15 +12,30 @@ * * @author Colin Doig */ -class RegexpReplace extends BaseRegexpFunction +class RegexpReplace extends BaseVariadicFunction { + protected function getNodeMappingPattern(): array + { + return [ + 'StringPrimary,StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary', + 'StringPrimary,StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary', + 'StringPrimary,StringPrimary,StringPrimary,StringPrimary', + 'StringPrimary,StringPrimary,StringPrimary', + ]; + } + protected function getFunctionName(): string { return 'regexp_replace'; } - protected function getParameterCount(): int + protected function getMinArgumentCount(): int { return 3; } + + protected function getMaxArgumentCount(): int + { + return 6; + } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstr.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstr.php new file mode 100644 index 00000000..00557712 --- /dev/null +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstr.php @@ -0,0 +1,46 @@ + + * + * @example Using it in DQL: "SELECT REGEXP_SUBSTR(e.text, 'c(.)(..)', 1, 1, 'i', 2) FROM Entity e" + */ +class RegexpSubstr extends BaseVariadicFunction +{ + protected function getNodeMappingPattern(): array + { + return [ + 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary,ArithmeticPrimary', + 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary', + 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary', + 'StringPrimary,StringPrimary', + ]; + } + + protected function getFunctionName(): string + { + return 'regexp_substr'; + } + + protected function getMinArgumentCount(): int + { + return 2; + } + + protected function getMaxArgumentCount(): int + { + return 6; + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCountTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCountTest.php new file mode 100644 index 00000000..8a0ebe66 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCountTest.php @@ -0,0 +1,65 @@ + RegexpCount::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'counts digits' => "SELECT regexp_count(c0_.text1, '\\d\\d\\d') AS sclr_0 FROM ContainsTexts c0_", + 'counts words' => "SELECT regexp_count(c0_.text1, '\\w+') AS sclr_0 FROM ContainsTexts c0_", + 'with start position' => "SELECT regexp_count(c0_.text1, '\\d\\d\\d', 1) AS sclr_0 FROM ContainsTexts c0_", + 'with flags' => "SELECT regexp_count(c0_.text1, '\\d\\d\\d', 1, 'i') AS sclr_0 FROM ContainsTexts c0_", + 'with all parameters' => "SELECT regexp_count(c0_.text1, '\\d\\d\\d', 1, 'i') AS sclr_0 FROM ContainsTexts c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'counts digits' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d') FROM %s e", ContainsTexts::class), + 'counts words' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\w+') FROM %s e", ContainsTexts::class), + 'with start position' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d', 1) FROM %s e", ContainsTexts::class), + 'with flags' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d', 1, 'i') FROM %s e", ContainsTexts::class), + 'with all parameters' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d', 1, 'i') FROM %s e", ContainsTexts::class), + ]; + } + + public function test_too_few_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('regexp_count() requires at least 2 arguments'); + + $dql = \sprintf('SELECT REGEXP_COUNT(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_count() requires between 2 and 4 arguments'); + + $dql = \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d+', 1, 'i', 'extra_arg') FROM %s e", ContainsTexts::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } +} diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstrTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstrTest.php new file mode 100644 index 00000000..03a29aca --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstrTest.php @@ -0,0 +1,67 @@ + RegexpInstr::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'finds position of pattern' => "SELECT regexp_instr(c0_.text1, 'c(.)(...)') AS sclr_0 FROM ContainsTexts c0_", + 'finds position of digits' => "SELECT regexp_instr(c0_.text1, '\\d+') AS sclr_0 FROM ContainsTexts c0_", + 'with start position' => "SELECT regexp_instr(c0_.text1, '\\d+', 1) AS sclr_0 FROM ContainsTexts c0_", + 'with start and occurrence' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2) AS sclr_0 FROM ContainsTexts c0_", + 'with start, occurrence and return_option' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2, 0) AS sclr_0 FROM ContainsTexts c0_", + 'with all parameters' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2, 0, 'i') AS sclr_0 FROM ContainsTexts c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'finds position of pattern' => \sprintf("SELECT REGEXP_INSTR(e.text1, 'c(.)(...)') FROM %s e", ContainsTexts::class), + 'finds position of digits' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+') FROM %s e", ContainsTexts::class), + 'with start position' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1) FROM %s e", ContainsTexts::class), + 'with start and occurrence' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2) FROM %s e", ContainsTexts::class), + 'with start, occurrence and return_option' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2, 0) FROM %s e", ContainsTexts::class), + 'with all parameters' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2, 0, 'i') FROM %s e", ContainsTexts::class), + ]; + } + + public function test_too_few_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('regexp_instr() requires at least 2 arguments'); + + $dql = \sprintf('SELECT REGEXP_INSTR(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_instr() requires between 2 and 6 arguments'); + + $dql = \sprintf("SELECT REGEXP_INSTR(e.text1, 'c(.)(..)', 1, 1, 0, '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 4e8d46a5..983e8fd5 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplaceTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplaceTest.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\RegexpReplace; -class RegexpReplaceTest extends TestCase +class RegexpReplaceTest extends BaseVariadicFunctionTestCase { + protected function createFixture(): BaseVariadicFunction + { + return new RegexpReplace('REGEXP_REPLACE'); + } + protected function getStringFunctions(): array { return [ @@ -19,14 +26,38 @@ protected function getStringFunctions(): array protected function getExpectedSqlStatements(): array { return [ - "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement') AS sclr_0 FROM ContainsTexts c0_", + 'basic replacement' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement') AS sclr_0 FROM ContainsTexts c0_", + 'with flags' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 'i') AS sclr_0 FROM ContainsTexts c0_", + 'with start position' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 'i', 3) AS sclr_0 FROM ContainsTexts c0_", + 'with occurrence count' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 'i', 3, 2) AS sclr_0 FROM ContainsTexts c0_", ]; } protected function getDqlStatements(): array { return [ - \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement') FROM %s e", ContainsTexts::class), + 'basic replacement' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement') FROM %s e", ContainsTexts::class), + 'with flags' => \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', 'i', 3) FROM %s e", ContainsTexts::class), + 'with occurrence count' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 'i', 3, 2) FROM %s e", ContainsTexts::class), ]; } + + public function test_too_few_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('regexp_replace() requires at least 3 arguments'); + + $dql = \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern') 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_replace() requires between 3 and 6 arguments'); + + $dql = \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 'i', 3, 2, 'extra_arg') FROM %s e", ContainsTexts::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstrTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstrTest.php new file mode 100644 index 00000000..de155469 --- /dev/null +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstrTest.php @@ -0,0 +1,65 @@ + RegexpSubstr::class, + ]; + } + + protected function getExpectedSqlStatements(): array + { + return [ + 'extracts pattern' => "SELECT regexp_substr(c0_.text1, 'c(.)(...)') AS sclr_0 FROM ContainsTexts c0_", + 'extracts digits' => "SELECT regexp_substr(c0_.text1, '\\d+') AS sclr_0 FROM ContainsTexts c0_", + 'extracts digits with start and N parameters' => "SELECT regexp_substr(c0_.text1, '\\d+', 1, 4) AS sclr_0 FROM ContainsTexts c0_", + 'extracts digits with start and N parameters and flags' => "SELECT regexp_substr(c0_.text1, '\\d+', 1, 4, 'i') AS sclr_0 FROM ContainsTexts c0_", + 'extracts digits with start, N and subexpr parameters and flags' => "SELECT regexp_substr(c0_.text1, '\\d+', 1, 4, 'i', 3) AS sclr_0 FROM ContainsTexts c0_", + ]; + } + + protected function getDqlStatements(): array + { + return [ + 'extracts pattern' => \sprintf("SELECT REGEXP_SUBSTR(e.text1, 'c(.)(...)') FROM %s e", ContainsTexts::class), + 'extracts digits' => \sprintf("SELECT REGEXP_SUBSTR(e.text1, '\\d+') FROM %s e", ContainsTexts::class), + 'extracts digits with start and N parameters' => \sprintf("SELECT REGEXP_SUBSTR(e.text1, '\\d+', 1, 4) FROM %s e", ContainsTexts::class), + 'extracts digits with start and N parameters and flags' => \sprintf("SELECT REGEXP_SUBSTR(e.text1, '\\d+', 1, 4, 'i') FROM %s e", ContainsTexts::class), + 'extracts digits with start, N and subexpr parameters and flags' => \sprintf("SELECT REGEXP_SUBSTR(e.text1, '\\d+', 1, 4, 'i', 3) FROM %s e", ContainsTexts::class), + ]; + } + + public function test_too_few_arguments_throws_exception(): void + { + $this->expectException(InvalidArgumentForVariadicFunctionException::class); + $this->expectExceptionMessage('regexp_substr() requires at least 2 arguments'); + + $dql = \sprintf('SELECT REGEXP_SUBSTR(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_substr() requires between 2 and 6 arguments'); + + $dql = \sprintf("SELECT REGEXP_SUBSTR(e.text1, 'c(.)(..)', 1, 1, 'i', 1, 'extra_arg') FROM %s e", ContainsTexts::class); + $this->buildEntityManager()->createQuery($dql)->getSQL(); + } +} From 4cde68c4acbcf2f1a24093452c8c59e4418254c9 Mon Sep 17 00:00:00 2001 From: Martin Georgiev Date: Mon, 21 Apr 2025 02:08:29 +0100 Subject: [PATCH 2/2] no message --- .../ORM/Query/AST/Functions/RegexpCount.php | 3 ++- .../ORM/Query/AST/Functions/RegexpInstr.php | 9 ++++++--- .../ORM/Query/AST/Functions/RegexpReplace.php | 12 +++++++++--- .../ORM/Query/AST/Functions/RegexpSubstr.php | 1 + .../Query/AST/Functions/RegexpCountTest.php | 2 -- .../Query/AST/Functions/RegexpInstrTest.php | 10 ++++++---- .../Query/AST/Functions/RegexpReplaceTest.php | 18 +++++++++++------- .../Query/AST/Functions/RegexpSubstrTest.php | 4 ++-- 8 files changed, 37 insertions(+), 22 deletions(-) diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCount.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCount.php index dc46da50..0d601e49 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCount.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCount.php @@ -14,7 +14,7 @@ * * @author Martin Georgiev * - * @example Using it in DQL: "SELECT REGEXP_COUNT(e.text, '\d\d\d') FROM Entity e" + * @example Using it in DQL: "SELECT REGEXP_COUNT(e.text, '\d\d\d', 1, 'i') FROM Entity e" */ class RegexpCount extends BaseVariadicFunction { @@ -22,6 +22,7 @@ protected function getNodeMappingPattern(): array { return [ 'StringPrimary,StringPrimary,ArithmeticPrimary,StringPrimary', + 'StringPrimary,StringPrimary,ArithmeticPrimary', 'StringPrimary,StringPrimary', ]; } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstr.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstr.php index 2007e3c5..18989a82 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstr.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstr.php @@ -10,20 +10,23 @@ * Returns the position within string where the Nth match of the POSIX regular expression pattern occurs, * or zero if there is no such match. * - * @see https://www.postgresql.org/docs/15/functions-matching.html#FUNCTIONS-POSIX-REGEXP + * @see https://www.postgresql.org/docs/17/functions-matching.html#FUNCTIONS-POSIX-REGEXP * @since 3.1 * * @author Martin Georgiev * - * @example Using it in DQL: "SELECT REGEXP_INSTR(e.text, 'c(.)(..)', 1, 1, 0, 'i') FROM Entity e" + * @example Using it in DQL: "SELECT REGEXP_INSTR(e.text, 'c(.)(..)', 1, 1, 0, 'i', 2) FROM Entity e" */ class RegexpInstr extends BaseVariadicFunction { protected function getNodeMappingPattern(): array { return [ + 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary,ArithmeticPrimary', 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary', 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,ArithmeticPrimary', + 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary', + 'StringPrimary,StringPrimary,ArithmeticPrimary', 'StringPrimary,StringPrimary', ]; } @@ -40,6 +43,6 @@ protected function getMinArgumentCount(): int protected function getMaxArgumentCount(): int { - return 6; + return 7; } } diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php index a3beba29..2a426fb2 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php @@ -7,18 +7,24 @@ /** * Implementation of PostgreSQL REGEXP_REPLACE(). * - * @see https://www.postgresql.org/docs/15/functions-matching.html#FUNCTIONS-POSIX-REGEXP + * Replaces substring(s) matching a POSIX regular expression pattern with a replacement string. + * + * @see https://www.postgresql.org/docs/17/functions-matching.html#FUNCTIONS-POSIX-REGEXP * @since 2.5 * * @author Colin Doig + * + * @example Using it in DQL: "SELECT REGEXP_REPLACE(e.text, 'pattern', 'replacement', 3, 2, 'i') FROM Entity e" */ class RegexpReplace extends BaseVariadicFunction { protected function getNodeMappingPattern(): array { return [ - 'StringPrimary,StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary', - 'StringPrimary,StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary', + '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', ]; diff --git a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstr.php b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstr.php index 00557712..72441d80 100644 --- a/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstr.php +++ b/src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstr.php @@ -25,6 +25,7 @@ protected function getNodeMappingPattern(): array 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary,ArithmeticPrimary', 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary', 'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary', + 'StringPrimary,StringPrimary,ArithmeticPrimary', 'StringPrimary,StringPrimary', ]; } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCountTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCountTest.php index 8a0ebe66..88b871b2 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCountTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpCountTest.php @@ -30,7 +30,6 @@ protected function getExpectedSqlStatements(): array 'counts words' => "SELECT regexp_count(c0_.text1, '\\w+') AS sclr_0 FROM ContainsTexts c0_", 'with start position' => "SELECT regexp_count(c0_.text1, '\\d\\d\\d', 1) AS sclr_0 FROM ContainsTexts c0_", 'with flags' => "SELECT regexp_count(c0_.text1, '\\d\\d\\d', 1, 'i') AS sclr_0 FROM ContainsTexts c0_", - 'with all parameters' => "SELECT regexp_count(c0_.text1, '\\d\\d\\d', 1, 'i') AS sclr_0 FROM ContainsTexts c0_", ]; } @@ -41,7 +40,6 @@ protected function getDqlStatements(): array 'counts words' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\w+') FROM %s e", ContainsTexts::class), 'with start position' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d', 1) FROM %s e", ContainsTexts::class), 'with flags' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d', 1, 'i') FROM %s e", ContainsTexts::class), - 'with all parameters' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d', 1, 'i') FROM %s e", ContainsTexts::class), ]; } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstrTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstrTest.php index 03a29aca..9961699b 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstrTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpInstrTest.php @@ -31,7 +31,8 @@ protected function getExpectedSqlStatements(): array 'with start position' => "SELECT regexp_instr(c0_.text1, '\\d+', 1) AS sclr_0 FROM ContainsTexts c0_", 'with start and occurrence' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2) AS sclr_0 FROM ContainsTexts c0_", 'with start, occurrence and return_option' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2, 0) AS sclr_0 FROM ContainsTexts c0_", - 'with all parameters' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2, 0, 'i') AS sclr_0 FROM ContainsTexts c0_", + 'with flags' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2, 0, 'i') AS sclr_0 FROM ContainsTexts c0_", + 'with subexpr' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2, 0, 'i', 2) AS sclr_0 FROM ContainsTexts c0_", ]; } @@ -43,7 +44,8 @@ protected function getDqlStatements(): array 'with start position' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1) FROM %s e", ContainsTexts::class), 'with start and occurrence' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2) FROM %s e", ContainsTexts::class), 'with start, occurrence and return_option' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2, 0) FROM %s e", ContainsTexts::class), - 'with all parameters' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2, 0, 'i') FROM %s e", ContainsTexts::class), + 'with flags' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2, 0, 'i') FROM %s e", ContainsTexts::class), + 'with subexpr' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2, 0, 'i', 2) FROM %s e", ContainsTexts::class), ]; } @@ -59,9 +61,9 @@ public function test_too_few_arguments_throws_exception(): void public function test_too_many_arguments_throws_exception(): void { $this->expectException(InvalidArgumentForVariadicFunctionException::class); - $this->expectExceptionMessage('regexp_instr() requires between 2 and 6 arguments'); + $this->expectExceptionMessage('regexp_instr() requires between 2 and 7 arguments'); - $dql = \sprintf("SELECT REGEXP_INSTR(e.text1, 'c(.)(..)', 1, 1, 0, 'i', 'extra_arg') FROM %s e", ContainsTexts::class); + $dql = \sprintf("SELECT REGEXP_INSTR(e.text1, 'c(.)(..)', 1, 1, 0, 'i', 2, '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 983e8fd5..17ea390d 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplaceTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplaceTest.php @@ -27,9 +27,11 @@ protected function getExpectedSqlStatements(): array { return [ 'basic replacement' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement') AS sclr_0 FROM ContainsTexts c0_", - 'with flags' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 'i') AS sclr_0 FROM ContainsTexts c0_", - 'with start position' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 'i', 3) AS sclr_0 FROM ContainsTexts c0_", - 'with occurrence count' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 'i', 3, 2) 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_", ]; } @@ -37,9 +39,11 @@ protected function getDqlStatements(): array { return [ 'basic replacement' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement') FROM %s e", ContainsTexts::class), - 'with flags' => \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', 'i', 3) FROM %s e", ContainsTexts::class), - 'with occurrence count' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 'i', 3, 2) 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), ]; } @@ -57,7 +61,7 @@ public function test_too_many_arguments_throws_exception(): void $this->expectException(InvalidArgumentForVariadicFunctionException::class); $this->expectExceptionMessage('regexp_replace() requires between 3 and 6 arguments'); - $dql = \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 'i', 3, 2, 'extra_arg') FROM %s e", ContainsTexts::class); + $dql = \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 3, 2, 'i', 'extra_arg') FROM %s e", ContainsTexts::class); $this->buildEntityManager()->createQuery($dql)->getSQL(); } } diff --git a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstrTest.php b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstrTest.php index de155469..8abb2f28 100644 --- a/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstrTest.php +++ b/tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpSubstrTest.php @@ -30,7 +30,7 @@ protected function getExpectedSqlStatements(): array 'extracts digits' => "SELECT regexp_substr(c0_.text1, '\\d+') AS sclr_0 FROM ContainsTexts c0_", 'extracts digits with start and N parameters' => "SELECT regexp_substr(c0_.text1, '\\d+', 1, 4) AS sclr_0 FROM ContainsTexts c0_", 'extracts digits with start and N parameters and flags' => "SELECT regexp_substr(c0_.text1, '\\d+', 1, 4, 'i') AS sclr_0 FROM ContainsTexts c0_", - 'extracts digits with start, N and subexpr parameters and flags' => "SELECT regexp_substr(c0_.text1, '\\d+', 1, 4, 'i', 3) AS sclr_0 FROM ContainsTexts c0_", + 'extracts digits with start, N parameters, flags and subexpr parameter' => "SELECT regexp_substr(c0_.text1, '\\d+', 1, 4, 'i', 3) AS sclr_0 FROM ContainsTexts c0_", ]; } @@ -41,7 +41,7 @@ protected function getDqlStatements(): array 'extracts digits' => \sprintf("SELECT REGEXP_SUBSTR(e.text1, '\\d+') FROM %s e", ContainsTexts::class), 'extracts digits with start and N parameters' => \sprintf("SELECT REGEXP_SUBSTR(e.text1, '\\d+', 1, 4) FROM %s e", ContainsTexts::class), 'extracts digits with start and N parameters and flags' => \sprintf("SELECT REGEXP_SUBSTR(e.text1, '\\d+', 1, 4, 'i') FROM %s e", ContainsTexts::class), - 'extracts digits with start, N and subexpr parameters and flags' => \sprintf("SELECT REGEXP_SUBSTR(e.text1, '\\d+', 1, 4, 'i', 3) FROM %s e", ContainsTexts::class), + 'extracts digits with start, N parameters, flags and subexpr parameter' => \sprintf("SELECT REGEXP_SUBSTR(e.text1, '\\d+', 1, 4, 'i', 3) FROM %s e", ContainsTexts::class), ]; }