Skip to content

Commit 9959476

Browse files
feat: add support for REGEXP_COUNT(), REGEXP_INSTR() and REGEXP_SUBSTR() and extend support for REGEXP_REPLACE() (#352)
1 parent 5be9c91 commit 9959476

File tree

8 files changed

+398
-6
lines changed

8 files changed

+398
-6
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostgreSQL REGEXP_COUNT().
9+
*
10+
* Returns the number of times the POSIX regular expression pattern matches in the string.
11+
*
12+
* @see https://www.postgresql.org/docs/15/functions-matching.html#FUNCTIONS-POSIX-REGEXP
13+
* @since 3.1
14+
*
15+
* @author Martin Georgiev <martin.georgiev@gmail.com>
16+
*
17+
* @example Using it in DQL: "SELECT REGEXP_COUNT(e.text, '\d\d\d', 1, 'i') FROM Entity e"
18+
*/
19+
class RegexpCount extends BaseVariadicFunction
20+
{
21+
protected function getNodeMappingPattern(): array
22+
{
23+
return [
24+
'StringPrimary,StringPrimary,ArithmeticPrimary,StringPrimary',
25+
'StringPrimary,StringPrimary,ArithmeticPrimary',
26+
'StringPrimary,StringPrimary',
27+
];
28+
}
29+
30+
protected function getFunctionName(): string
31+
{
32+
return 'regexp_count';
33+
}
34+
35+
protected function getMinArgumentCount(): int
36+
{
37+
return 2;
38+
}
39+
40+
protected function getMaxArgumentCount(): int
41+
{
42+
return 4;
43+
}
44+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostgreSQL REGEXP_INSTR().
9+
*
10+
* Returns the position within string where the Nth match of the POSIX regular expression pattern occurs,
11+
* or zero if there is no such match.
12+
*
13+
* @see https://www.postgresql.org/docs/17/functions-matching.html#FUNCTIONS-POSIX-REGEXP
14+
* @since 3.1
15+
*
16+
* @author Martin Georgiev <martin.georgiev@gmail.com>
17+
*
18+
* @example Using it in DQL: "SELECT REGEXP_INSTR(e.text, 'c(.)(..)', 1, 1, 0, 'i', 2) FROM Entity e"
19+
*/
20+
class RegexpInstr extends BaseVariadicFunction
21+
{
22+
protected function getNodeMappingPattern(): array
23+
{
24+
return [
25+
'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary,ArithmeticPrimary',
26+
'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary',
27+
'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,ArithmeticPrimary',
28+
'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary',
29+
'StringPrimary,StringPrimary,ArithmeticPrimary',
30+
'StringPrimary,StringPrimary',
31+
];
32+
}
33+
34+
protected function getFunctionName(): string
35+
{
36+
return 'regexp_instr';
37+
}
38+
39+
protected function getMinArgumentCount(): int
40+
{
41+
return 2;
42+
}
43+
44+
protected function getMaxArgumentCount(): int
45+
{
46+
return 7;
47+
}
48+
}

src/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplace.php

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,41 @@
77
/**
88
* Implementation of PostgreSQL REGEXP_REPLACE().
99
*
10-
* @see https://www.postgresql.org/docs/15/functions-matching.html#FUNCTIONS-POSIX-REGEXP
10+
* Replaces substring(s) matching a POSIX regular expression pattern with a replacement string.
11+
*
12+
* @see https://www.postgresql.org/docs/17/functions-matching.html#FUNCTIONS-POSIX-REGEXP
1113
* @since 2.5
1214
*
1315
* @author Colin Doig
16+
*
17+
* @example Using it in DQL: "SELECT REGEXP_REPLACE(e.text, 'pattern', 'replacement', 3, 2, 'i') FROM Entity e"
1418
*/
15-
class RegexpReplace extends BaseRegexpFunction
19+
class RegexpReplace extends BaseVariadicFunction
1620
{
21+
protected function getNodeMappingPattern(): array
22+
{
23+
return [
24+
'StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary',
25+
'StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary',
26+
'StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary,StringPrimary',
27+
'StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary',
28+
'StringPrimary,StringPrimary,StringPrimary,StringPrimary',
29+
'StringPrimary,StringPrimary,StringPrimary',
30+
];
31+
}
32+
1733
protected function getFunctionName(): string
1834
{
1935
return 'regexp_replace';
2036
}
2137

22-
protected function getParameterCount(): int
38+
protected function getMinArgumentCount(): int
2339
{
2440
return 3;
2541
}
42+
43+
protected function getMaxArgumentCount(): int
44+
{
45+
return 6;
46+
}
2647
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
/**
8+
* Implementation of PostgreSQL REGEXP_SUBSTR().
9+
*
10+
* Returns the substring within string that matches the Nth occurrence of the POSIX regular expression pattern,
11+
* or NULL if there is no such match.
12+
*
13+
* @see https://www.postgresql.org/docs/15/functions-matching.html#FUNCTIONS-POSIX-REGEXP
14+
* @since 3.1
15+
*
16+
* @author Martin Georgiev <martin.georgiev@gmail.com>
17+
*
18+
* @example Using it in DQL: "SELECT REGEXP_SUBSTR(e.text, 'c(.)(..)', 1, 1, 'i', 2) FROM Entity e"
19+
*/
20+
class RegexpSubstr extends BaseVariadicFunction
21+
{
22+
protected function getNodeMappingPattern(): array
23+
{
24+
return [
25+
'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary,ArithmeticPrimary',
26+
'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,StringPrimary',
27+
'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary',
28+
'StringPrimary,StringPrimary,ArithmeticPrimary',
29+
'StringPrimary,StringPrimary',
30+
];
31+
}
32+
33+
protected function getFunctionName(): string
34+
{
35+
return 'regexp_substr';
36+
}
37+
38+
protected function getMinArgumentCount(): int
39+
{
40+
return 2;
41+
}
42+
43+
protected function getMaxArgumentCount(): int
44+
{
45+
return 6;
46+
}
47+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsTexts;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseVariadicFunction;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
10+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RegexpCount;
11+
12+
class RegexpCountTest extends BaseVariadicFunctionTestCase
13+
{
14+
protected function createFixture(): BaseVariadicFunction
15+
{
16+
return new RegexpCount('REGEXP_COUNT');
17+
}
18+
19+
protected function getStringFunctions(): array
20+
{
21+
return [
22+
'REGEXP_COUNT' => RegexpCount::class,
23+
];
24+
}
25+
26+
protected function getExpectedSqlStatements(): array
27+
{
28+
return [
29+
'counts digits' => "SELECT regexp_count(c0_.text1, '\\d\\d\\d') AS sclr_0 FROM ContainsTexts c0_",
30+
'counts words' => "SELECT regexp_count(c0_.text1, '\\w+') AS sclr_0 FROM ContainsTexts c0_",
31+
'with start position' => "SELECT regexp_count(c0_.text1, '\\d\\d\\d', 1) AS sclr_0 FROM ContainsTexts c0_",
32+
'with flags' => "SELECT regexp_count(c0_.text1, '\\d\\d\\d', 1, 'i') AS sclr_0 FROM ContainsTexts c0_",
33+
];
34+
}
35+
36+
protected function getDqlStatements(): array
37+
{
38+
return [
39+
'counts digits' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d') FROM %s e", ContainsTexts::class),
40+
'counts words' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\w+') FROM %s e", ContainsTexts::class),
41+
'with start position' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d', 1) FROM %s e", ContainsTexts::class),
42+
'with flags' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d', 1, 'i') FROM %s e", ContainsTexts::class),
43+
];
44+
}
45+
46+
public function test_too_few_arguments_throws_exception(): void
47+
{
48+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
49+
$this->expectExceptionMessage('regexp_count() requires at least 2 arguments');
50+
51+
$dql = \sprintf('SELECT REGEXP_COUNT(e.text1) FROM %s e', ContainsTexts::class);
52+
$this->buildEntityManager()->createQuery($dql)->getSQL();
53+
}
54+
55+
public function test_too_many_arguments_throws_exception(): void
56+
{
57+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
58+
$this->expectExceptionMessage('regexp_count() requires between 2 and 4 arguments');
59+
60+
$dql = \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d+', 1, 'i', 'extra_arg') FROM %s e", ContainsTexts::class);
61+
$this->buildEntityManager()->createQuery($dql)->getSQL();
62+
}
63+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
6+
7+
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsTexts;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseVariadicFunction;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
10+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RegexpInstr;
11+
12+
class RegexpInstrTest extends BaseVariadicFunctionTestCase
13+
{
14+
protected function createFixture(): BaseVariadicFunction
15+
{
16+
return new RegexpInstr('REGEXP_INSTR');
17+
}
18+
19+
protected function getStringFunctions(): array
20+
{
21+
return [
22+
'REGEXP_INSTR' => RegexpInstr::class,
23+
];
24+
}
25+
26+
protected function getExpectedSqlStatements(): array
27+
{
28+
return [
29+
'finds position of pattern' => "SELECT regexp_instr(c0_.text1, 'c(.)(...)') AS sclr_0 FROM ContainsTexts c0_",
30+
'finds position of digits' => "SELECT regexp_instr(c0_.text1, '\\d+') AS sclr_0 FROM ContainsTexts c0_",
31+
'with start position' => "SELECT regexp_instr(c0_.text1, '\\d+', 1) AS sclr_0 FROM ContainsTexts c0_",
32+
'with start and occurrence' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2) AS sclr_0 FROM ContainsTexts c0_",
33+
'with start, occurrence and return_option' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2, 0) AS sclr_0 FROM ContainsTexts c0_",
34+
'with flags' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2, 0, 'i') AS sclr_0 FROM ContainsTexts c0_",
35+
'with subexpr' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2, 0, 'i', 2) AS sclr_0 FROM ContainsTexts c0_",
36+
];
37+
}
38+
39+
protected function getDqlStatements(): array
40+
{
41+
return [
42+
'finds position of pattern' => \sprintf("SELECT REGEXP_INSTR(e.text1, 'c(.)(...)') FROM %s e", ContainsTexts::class),
43+
'finds position of digits' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+') FROM %s e", ContainsTexts::class),
44+
'with start position' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1) FROM %s e", ContainsTexts::class),
45+
'with start and occurrence' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2) FROM %s e", ContainsTexts::class),
46+
'with start, occurrence and return_option' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2, 0) FROM %s e", ContainsTexts::class),
47+
'with flags' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2, 0, 'i') FROM %s e", ContainsTexts::class),
48+
'with subexpr' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2, 0, 'i', 2) FROM %s e", ContainsTexts::class),
49+
];
50+
}
51+
52+
public function test_too_few_arguments_throws_exception(): void
53+
{
54+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
55+
$this->expectExceptionMessage('regexp_instr() requires at least 2 arguments');
56+
57+
$dql = \sprintf('SELECT REGEXP_INSTR(e.text1) FROM %s e', ContainsTexts::class);
58+
$this->buildEntityManager()->createQuery($dql)->getSQL();
59+
}
60+
61+
public function test_too_many_arguments_throws_exception(): void
62+
{
63+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
64+
$this->expectExceptionMessage('regexp_instr() requires between 2 and 7 arguments');
65+
66+
$dql = \sprintf("SELECT REGEXP_INSTR(e.text1, 'c(.)(..)', 1, 1, 0, 'i', 2, 'extra_arg') FROM %s e", ContainsTexts::class);
67+
$this->buildEntityManager()->createQuery($dql)->getSQL();
68+
}
69+
}

tests/MartinGeorgiev/Doctrine/ORM/Query/AST/Functions/RegexpReplaceTest.php

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,17 @@
55
namespace Tests\MartinGeorgiev\Doctrine\ORM\Query\AST\Functions;
66

77
use Fixtures\MartinGeorgiev\Doctrine\Entity\ContainsTexts;
8+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\BaseVariadicFunction;
9+
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\Exception\InvalidArgumentForVariadicFunctionException;
810
use MartinGeorgiev\Doctrine\ORM\Query\AST\Functions\RegexpReplace;
911

10-
class RegexpReplaceTest extends TestCase
12+
class RegexpReplaceTest extends BaseVariadicFunctionTestCase
1113
{
14+
protected function createFixture(): BaseVariadicFunction
15+
{
16+
return new RegexpReplace('REGEXP_REPLACE');
17+
}
18+
1219
protected function getStringFunctions(): array
1320
{
1421
return [
@@ -19,14 +26,42 @@ protected function getStringFunctions(): array
1926
protected function getExpectedSqlStatements(): array
2027
{
2128
return [
22-
"SELECT regexp_replace(c0_.text1, 'pattern', 'replacement') AS sclr_0 FROM ContainsTexts c0_",
29+
'basic replacement' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement') AS sclr_0 FROM ContainsTexts c0_",
30+
'with flags but no start position' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 'i') AS sclr_0 FROM ContainsTexts c0_",
31+
'with start position' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 3) AS sclr_0 FROM ContainsTexts c0_",
32+
'with start position and flags' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 3, 'i') AS sclr_0 FROM ContainsTexts c0_",
33+
'with occurrence count but no flags' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 3, 2) AS sclr_0 FROM ContainsTexts c0_",
34+
'with occurrence count and flags' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 3, 2, 'i') AS sclr_0 FROM ContainsTexts c0_",
2335
];
2436
}
2537

2638
protected function getDqlStatements(): array
2739
{
2840
return [
29-
\sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement') FROM %s e", ContainsTexts::class),
41+
'basic replacement' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement') FROM %s e", ContainsTexts::class),
42+
'with flags but no start position' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 'i') FROM %s e", ContainsTexts::class),
43+
'with start position' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 3) FROM %s e", ContainsTexts::class),
44+
'with start position and flags' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 3, 'i') FROM %s e", ContainsTexts::class),
45+
'with occurrence count but no flags' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 3, 2) FROM %s e", ContainsTexts::class),
46+
'with occurrence count and flags' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 3, 2, 'i') FROM %s e", ContainsTexts::class),
3047
];
3148
}
49+
50+
public function test_too_few_arguments_throws_exception(): void
51+
{
52+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
53+
$this->expectExceptionMessage('regexp_replace() requires at least 3 arguments');
54+
55+
$dql = \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern') FROM %s e", ContainsTexts::class);
56+
$this->buildEntityManager()->createQuery($dql)->getSQL();
57+
}
58+
59+
public function test_too_many_arguments_throws_exception(): void
60+
{
61+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
62+
$this->expectExceptionMessage('regexp_replace() requires between 3 and 6 arguments');
63+
64+
$dql = \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 3, 2, 'i', 'extra_arg') FROM %s e", ContainsTexts::class);
65+
$this->buildEntityManager()->createQuery($dql)->getSQL();
66+
}
3267
}

0 commit comments

Comments
 (0)