Skip to content

Commit 45e5358

Browse files
feat: add support for REGEXP_COUNT(), REGEXP_INSTR() and REGEXP_SUBSTR() and extend support for REGEXP_REPLACE()
1 parent e111dd2 commit 45e5358

File tree

8 files changed

+382
-5
lines changed

8 files changed

+382
-5
lines changed
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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') 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',
26+
];
27+
}
28+
29+
protected function getFunctionName(): string
30+
{
31+
return 'regexp_count';
32+
}
33+
34+
protected function getMinArgumentCount(): int
35+
{
36+
return 2;
37+
}
38+
39+
protected function getMaxArgumentCount(): int
40+
{
41+
return 4;
42+
}
43+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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/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_INSTR(e.text, 'c(.)(..)', 1, 1, 0, 'i') 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',
26+
'StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary,ArithmeticPrimary',
27+
'StringPrimary,StringPrimary',
28+
];
29+
}
30+
31+
protected function getFunctionName(): string
32+
{
33+
return 'regexp_instr';
34+
}
35+
36+
protected function getMinArgumentCount(): int
37+
{
38+
return 2;
39+
}
40+
41+
protected function getMaxArgumentCount(): int
42+
{
43+
return 6;
44+
}
45+
}

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,30 @@
1212
*
1313
* @author Colin Doig
1414
*/
15-
class RegexpReplace extends BaseRegexpFunction
15+
class RegexpReplace extends BaseVariadicFunction
1616
{
17+
protected function getNodeMappingPattern(): array
18+
{
19+
return [
20+
'StringPrimary,StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary,ArithmeticPrimary',
21+
'StringPrimary,StringPrimary,StringPrimary,StringPrimary,ArithmeticPrimary',
22+
'StringPrimary,StringPrimary,StringPrimary,StringPrimary',
23+
'StringPrimary,StringPrimary,StringPrimary',
24+
];
25+
}
26+
1727
protected function getFunctionName(): string
1828
{
1929
return 'regexp_replace';
2030
}
2131

22-
protected function getParameterCount(): int
32+
protected function getMinArgumentCount(): int
2333
{
2434
return 3;
2535
}
36+
37+
protected function getMaxArgumentCount(): int
38+
{
39+
return 6;
40+
}
2641
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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',
29+
];
30+
}
31+
32+
protected function getFunctionName(): string
33+
{
34+
return 'regexp_substr';
35+
}
36+
37+
protected function getMinArgumentCount(): int
38+
{
39+
return 2;
40+
}
41+
42+
protected function getMaxArgumentCount(): int
43+
{
44+
return 6;
45+
}
46+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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+
'with all parameters' => "SELECT regexp_count(c0_.text1, '\\d\\d\\d', 1, 'i') AS sclr_0 FROM ContainsTexts c0_",
34+
];
35+
}
36+
37+
protected function getDqlStatements(): array
38+
{
39+
return [
40+
'counts digits' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d') FROM %s e", ContainsTexts::class),
41+
'counts words' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\w+') FROM %s e", ContainsTexts::class),
42+
'with start position' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d', 1) FROM %s e", ContainsTexts::class),
43+
'with flags' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d', 1, 'i') FROM %s e", ContainsTexts::class),
44+
'with all parameters' => \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d\\d\\d', 1, 'i') FROM %s e", ContainsTexts::class),
45+
];
46+
}
47+
48+
public function test_too_few_arguments_throws_exception(): void
49+
{
50+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
51+
$this->expectExceptionMessage('regexp_count() requires at least 2 arguments');
52+
53+
$dql = \sprintf('SELECT REGEXP_COUNT(e.text1) FROM %s e', ContainsTexts::class);
54+
$this->buildEntityManager()->createQuery($dql)->getSQL();
55+
}
56+
57+
public function test_too_many_arguments_throws_exception(): void
58+
{
59+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
60+
$this->expectExceptionMessage('regexp_count() requires between 2 and 4 arguments');
61+
62+
$dql = \sprintf("SELECT REGEXP_COUNT(e.text1, '\\d+', 1, 'i', 'extra_arg') FROM %s e", ContainsTexts::class);
63+
$this->buildEntityManager()->createQuery($dql)->getSQL();
64+
}
65+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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 all parameters' => "SELECT regexp_instr(c0_.text1, '\\d+', 1, 2, 0, 'i') AS sclr_0 FROM ContainsTexts c0_",
35+
];
36+
}
37+
38+
protected function getDqlStatements(): array
39+
{
40+
return [
41+
'finds position of pattern' => \sprintf("SELECT REGEXP_INSTR(e.text1, 'c(.)(...)') FROM %s e", ContainsTexts::class),
42+
'finds position of digits' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+') FROM %s e", ContainsTexts::class),
43+
'with start position' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1) FROM %s e", ContainsTexts::class),
44+
'with start and occurrence' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2) FROM %s e", ContainsTexts::class),
45+
'with start, occurrence and return_option' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2, 0) FROM %s e", ContainsTexts::class),
46+
'with all parameters' => \sprintf("SELECT REGEXP_INSTR(e.text1, '\\d+', 1, 2, 0, 'i') FROM %s e", ContainsTexts::class),
47+
];
48+
}
49+
50+
public function test_too_few_arguments_throws_exception(): void
51+
{
52+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
53+
$this->expectExceptionMessage('regexp_instr() requires at least 2 arguments');
54+
55+
$dql = \sprintf('SELECT REGEXP_INSTR(e.text1) 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_instr() requires between 2 and 6 arguments');
63+
64+
$dql = \sprintf("SELECT REGEXP_INSTR(e.text1, 'c(.)(..)', 1, 1, 0, 'i', 'extra_arg') FROM %s e", ContainsTexts::class);
65+
$this->buildEntityManager()->createQuery($dql)->getSQL();
66+
}
67+
}

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

Lines changed: 34 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,38 @@ 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' => "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', 'i', 3) AS sclr_0 FROM ContainsTexts c0_",
32+
'with occurrence count' => "SELECT regexp_replace(c0_.text1, 'pattern', 'replacement', 'i', 3, 2) AS sclr_0 FROM ContainsTexts c0_",
2333
];
2434
}
2535

2636
protected function getDqlStatements(): array
2737
{
2838
return [
29-
\sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement') FROM %s e", ContainsTexts::class),
39+
'basic replacement' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement') FROM %s e", ContainsTexts::class),
40+
'with flags' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 'i') FROM %s e", ContainsTexts::class),
41+
'with start position' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 'i', 3) FROM %s e", ContainsTexts::class),
42+
'with occurrence count' => \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 'i', 3, 2) FROM %s e", ContainsTexts::class),
3043
];
3144
}
45+
46+
public function test_too_few_arguments_throws_exception(): void
47+
{
48+
$this->expectException(InvalidArgumentForVariadicFunctionException::class);
49+
$this->expectExceptionMessage('regexp_replace() requires at least 3 arguments');
50+
51+
$dql = \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern') 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_replace() requires between 3 and 6 arguments');
59+
60+
$dql = \sprintf("SELECT REGEXP_REPLACE(e.text1, 'pattern', 'replacement', 'i', 3, 2, 'extra_arg') FROM %s e", ContainsTexts::class);
61+
$this->buildEntityManager()->createQuery($dql)->getSQL();
62+
}
3263
}

0 commit comments

Comments
 (0)