Skip to content

Commit 9280a57

Browse files
committed
Add support for Postgres placeholders to Parser
1 parent e9fb8d7 commit 9280a57

File tree

4 files changed

+114
-2
lines changed

4 files changed

+114
-2
lines changed

src/Driver/PgSQL/ConvertParameters.php

+10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
use function count;
1010
use function implode;
11+
use function ltrim;
12+
use function str_starts_with;
1113

1214
final class ConvertParameters implements Visitor
1315
{
@@ -19,6 +21,14 @@ final class ConvertParameters implements Visitor
1921

2022
public function acceptPositionalParameter(string $sql): void
2123
{
24+
if (str_starts_with($sql, '$')) {
25+
$position = (int) ltrim($sql, '$');
26+
$this->parameterMap[$position] = $position;
27+
$this->buffer[] = $sql;
28+
29+
return;
30+
}
31+
2232
$position = count($this->parameterMap) + 1;
2333
$this->parameterMap[$position] = $position;
2434
$this->buffer[] = '$' . $position;

src/SQL/Parser.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@
3434
*/
3535
final class Parser
3636
{
37-
private const SPECIAL_CHARS = ':\?\'"`\\[\\-\\/';
37+
private const SPECIAL_CHARS = ':\?\'"`\\[\\-\\/$';
3838

3939
private const BACKTICK_IDENTIFIER = '`[^`]*`';
4040
private const BRACKET_IDENTIFIER = '(?<!\b(?i:ARRAY))\[(?:[^\]])*\]';
4141
private const MULTICHAR = ':{2,}';
4242
private const NAMED_PARAMETER = ':[a-zA-Z0-9_]+';
43-
private const POSITIONAL_PARAMETER = '(?<!\\?)\\?(?!\\?)';
43+
private const POSITIONAL_PARAMETER = '((?<!\\?)\\?(?!\\?)|\\$\d+)';
4444
private const ONE_LINE_COMMENT = '--[^\r\n]*';
4545
private const MULTI_LINE_COMMENT = '/\*([^*]+|\*+[^/*])*\**\*/';
4646
private const SPECIAL = '[' . self::SPECIAL_CHARS . ']';

tests/Functional/SQL/ParserTest.php

+17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Doctrine\DBAL\Tests\Functional\SQL;
66

7+
use Doctrine\DBAL\ParameterType;
78
use Doctrine\DBAL\Tests\FunctionalTestCase;
89
use Doctrine\DBAL\Tests\TestUtil;
910

@@ -17,4 +18,20 @@ public function testPostgreSQLJSONBQuestionOperator(): void
1718

1819
self::assertTrue($this->connection->fetchOne('SELECT \'{"a":null}\'::jsonb ?? :key', ['key' => 'a']));
1920
}
21+
22+
public function testPostgresPlaceholders(): void
23+
{
24+
if (! TestUtil::isDriverOneOf('pgsql')) {
25+
self::markTestSkipped('This test requires the pgsql driver.');
26+
}
27+
28+
$this->connection->beginTransaction();
29+
$this->connection->executeStatement('CREATE TABLE dummy_table (a_number smallint, a_number_2 smallint)');
30+
$this->connection->executeStatement(
31+
'INSERT INTO dummy_table (a_number, a_number_2) VALUES ($1, $1)',
32+
[1],
33+
[ParameterType::INTEGER],
34+
);
35+
$this->connection->rollBack();
36+
}
2037
}

tests/SQL/ParserTest.php

+85
Original file line numberDiff line numberDiff line change
@@ -45,61 +45,121 @@ private static function getStatementsWithParameters(): iterable
4545
'SELECT {?}',
4646
];
4747

48+
yield [
49+
'SELECT $1',
50+
'SELECT {$1}',
51+
];
52+
4853
yield [
4954
'SELECT * FROM Foo WHERE bar IN (?, ?, ?)',
5055
'SELECT * FROM Foo WHERE bar IN ({?}, {?}, {?})',
5156
];
5257

58+
yield [
59+
'SELECT * FROM Foo WHERE bar IN ($1, $2, $1)',
60+
'SELECT * FROM Foo WHERE bar IN ({$1}, {$2}, {$1})',
61+
];
62+
5363
yield [
5464
'SELECT ? FROM ?',
5565
'SELECT {?} FROM {?}',
5666
];
5767

68+
yield [
69+
'SELECT $1 FROM $2',
70+
'SELECT {$1} FROM {$2}',
71+
];
72+
5873
yield [
5974
'SELECT "?" FROM foo WHERE bar = ?',
6075
'SELECT "?" FROM foo WHERE bar = {?}',
6176
];
6277

78+
yield [
79+
'SELECT "$1" FROM foo WHERE bar = $1',
80+
'SELECT "$1" FROM foo WHERE bar = {$1}',
81+
];
82+
6383
yield [
6484
"SELECT '?' FROM foo WHERE bar = ?",
6585
"SELECT '?' FROM foo WHERE bar = {?}",
6686
];
6787

88+
yield [
89+
"SELECT '$1' FROM foo WHERE bar = $1",
90+
"SELECT '$1' FROM foo WHERE bar = {\$1}",
91+
];
92+
6893
yield [
6994
'SELECT `?` FROM foo WHERE bar = ?',
7095
'SELECT `?` FROM foo WHERE bar = {?}',
7196
];
7297

98+
yield [
99+
'SELECT `$1` FROM foo WHERE bar = $1',
100+
'SELECT `$1` FROM foo WHERE bar = {$1}',
101+
];
102+
73103
yield [
74104
'SELECT [?] FROM foo WHERE bar = ?',
75105
'SELECT [?] FROM foo WHERE bar = {?}',
76106
];
77107

108+
yield [
109+
'SELECT [$1] FROM foo WHERE bar = $1',
110+
'SELECT [$1] FROM foo WHERE bar = {$1}',
111+
];
112+
78113
yield [
79114
'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[?])',
80115
'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[{?}])',
81116
];
82117

118+
yield [
119+
'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[$1])',
120+
'SELECT * FROM foo WHERE jsonb_exists_any(foo.bar, ARRAY[{$1}])',
121+
];
122+
83123
yield [
84124
"SELECT 'Doctrine\DBAL?' FROM foo WHERE bar = ?",
85125
"SELECT 'Doctrine\DBAL?' FROM foo WHERE bar = {?}",
86126
];
87127

128+
yield [
129+
"SELECT 'Doctrine\DBAL$1' FROM foo WHERE bar = $1",
130+
"SELECT 'Doctrine\DBAL$1' FROM foo WHERE bar = {\$1}",
131+
];
132+
88133
yield [
89134
'SELECT "Doctrine\DBAL?" FROM foo WHERE bar = ?',
90135
'SELECT "Doctrine\DBAL?" FROM foo WHERE bar = {?}',
91136
];
92137

138+
yield [
139+
'SELECT "Doctrine\DBAL$1" FROM foo WHERE bar = $1',
140+
'SELECT "Doctrine\DBAL$1" FROM foo WHERE bar = {$1}',
141+
];
142+
93143
yield [
94144
'SELECT `Doctrine\DBAL?` FROM foo WHERE bar = ?',
95145
'SELECT `Doctrine\DBAL?` FROM foo WHERE bar = {?}',
96146
];
97147

148+
yield [
149+
'SELECT `Doctrine\DBAL$1` FROM foo WHERE bar = $1',
150+
'SELECT `Doctrine\DBAL$1` FROM foo WHERE bar = {$1}',
151+
];
152+
98153
yield [
99154
'SELECT [Doctrine\DBAL?] FROM foo WHERE bar = ?',
100155
'SELECT [Doctrine\DBAL?] FROM foo WHERE bar = {?}',
101156
];
102157

158+
yield [
159+
'SELECT [Doctrine\DBAL?] FROM foo WHERE bar = $1',
160+
'SELECT [Doctrine\DBAL?] FROM foo WHERE bar = {$1}',
161+
];
162+
103163
yield [
104164
'SELECT :foo FROM :bar',
105165
'SELECT {:foo} FROM {:bar}',
@@ -293,6 +353,31 @@ private static function getStatementsWithParameters(): iterable
293353
,
294354
];
295355

356+
yield 'Postgres placeholders inside comments' => [
357+
<<<'SQL'
358+
/*
359+
* test placeholder $1
360+
*/
361+
SELECT dummy as "dummy$1"
362+
FROM DUAL
363+
WHERE '$1' = '$1'
364+
-- AND dummy <> $1
365+
AND dummy = $1
366+
SQL
367+
,
368+
<<<'SQL'
369+
/*
370+
* test placeholder $1
371+
*/
372+
SELECT dummy as "dummy$1"
373+
FROM DUAL
374+
WHERE '$1' = '$1'
375+
-- AND dummy <> $1
376+
AND dummy = {$1}
377+
SQL
378+
,
379+
];
380+
296381
yield 'Escaped question' => [
297382
<<<'SQL'
298383
SELECT '{"a":null}'::jsonb ?? :key

0 commit comments

Comments
 (0)