Skip to content

Commit c75d358

Browse files
committed
Add support for Postgres positional parameters to Parser
1 parent b299d3b commit c75d358

File tree

4 files changed

+139
-2
lines changed

4 files changed

+139
-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 . ']';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\DBAL\Tests\Functional\SQL;
6+
7+
use Doctrine\DBAL\ParameterType;
8+
use Doctrine\DBAL\Schema\Table;
9+
use Doctrine\DBAL\Tests\FunctionalTestCase;
10+
use Doctrine\DBAL\Tests\TestUtil;
11+
use Doctrine\DBAL\Types\Types;
12+
13+
final class PostgresNativePositionalParametersTest extends FunctionalTestCase
14+
{
15+
public function testPostgresNativePositionalParameters(): void
16+
{
17+
if (! TestUtil::isDriverOneOf('pgsql')) {
18+
self::markTestSkipped('This test requires the pgsql driver.');
19+
}
20+
21+
$table = new Table('dummy_table');
22+
$table->addColumn('a_number', Types::SMALLINT);
23+
$table->addColumn('a_number_2', Types::SMALLINT);
24+
$table->addColumn('b_number', Types::SMALLINT);
25+
$table->addColumn('c_number', Types::SMALLINT);
26+
$table->addColumn('a_number_3', Types::SMALLINT);
27+
$this->dropAndCreateTable($table);
28+
$this->connection->executeStatement(
29+
'INSERT INTO dummy_table (a_number, a_number_2, b_number, c_number, a_number_3)' .
30+
' VALUES ($1, $1, $2, $3, $1)',
31+
[1, 2, 3],
32+
[ParameterType::INTEGER, ParameterType::INTEGER, ParameterType::INTEGER],
33+
);
34+
$result = $this->connection->executeQuery('SELECT * FROM dummy_table')->fetchAllAssociative();
35+
self::assertCount(1, $result);
36+
self::assertEquals(1, $result[0]['a_number']);
37+
self::assertEquals(1, $result[0]['a_number_2']);
38+
self::assertEquals(2, $result[0]['b_number']);
39+
self::assertEquals(3, $result[0]['c_number']);
40+
self::assertEquals(1, $result[0]['a_number_3']);
41+
}
42+
}

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)