Skip to content

Commit 6b1cea3

Browse files
authored
Fixed case where operators containing question mark were erroneously substituted by the parameter parser (#40)
1 parent b1a2f34 commit 6b1cea3

File tree

2 files changed

+82
-3
lines changed

2 files changed

+82
-3
lines changed

src/Internal/functions.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,28 @@
55
use function Amp\Postgres\cast;
66

77
const STATEMENT_PARAM_REGEX = <<<'REGEX'
8-
~(["'`])(?:\\(?:\\|\1)|(?!\1).)*+\1(*SKIP)(*FAIL)|(\$(\d+)|\?)|(?<!:):([a-zA-Z_][a-zA-Z0-9_]*)~ms
8+
[
9+
# Skip all quoted groups.
10+
(['"])(?:\\(?:\\|\1)|(?!\1).)*+\1(*SKIP)(*FAIL)
11+
|
12+
# Unnamed parameters.
13+
(
14+
\$(\d+)
15+
|
16+
# Match all question marks except those surrounded by "operator"-class characters on either side.
17+
(?<!(?<operators>[-+\\*/<>~!@#%^&|`?]))
18+
\?
19+
(?!\g<operators>|=)
20+
)
21+
|
22+
# Named parameters.
23+
(?<!:):([a-zA-Z_][a-zA-Z0-9_]*)
24+
]msxS
925
REGEX;
1026

1127
/**
1228
* @param string $sql SQL statement with named and unnamed placeholders.
13-
* @param array $names [Output] Array of parameter positions mapped to names and/or indexed locations.
29+
* @param array|null $names [Output] Array of parameter positions mapped to names and/or indexed locations.
1430
*
1531
* @return string SQL statement with Postgres-style placeholders
1632
*/
@@ -21,7 +37,7 @@ function parseNamedParams(string $sql, ?array &$names): string
2137
static $index = 0, $unnamed = 0, $numbered = 1;
2238

2339
if (isset($matches[4])) {
24-
$names[$index] = $matches[4];
40+
$names[$index] = $matches[5];
2541
} elseif ($matches[2] === '?') {
2642
$names[$index] = $unnamed++;
2743
} else {

test/InternalFunctionsTest.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Amp\Postgres\Test;
5+
6+
use PHPUnit\Framework\TestCase;
7+
use function Amp\Postgres\Internal\parseNamedParams;
8+
9+
final class InternalFunctionsTest extends TestCase
10+
{
11+
/**
12+
* Tests that unnamed parameters are substituted correctly.
13+
*
14+
* @see parseNamedParams
15+
* @dataProvider provideUnnamedParams
16+
*/
17+
public function testParseUnnamedParams(string $sqlIn, string $sqlOut): void
18+
{
19+
self::assertSame($sqlOut, parseNamedParams($sqlIn, $dummy));
20+
}
21+
22+
public function provideUnnamedParams(): iterable
23+
{
24+
return [
25+
'Bare' => ['SELECT ?', 'SELECT $1'],
26+
'Parenthesized' => ['SELECT (?)', 'SELECT ($1)'],
27+
'Row constructor' => ['SELECT (?, ?)', 'SELECT ($1, $2)'],
28+
// Special-case exclude the =? operator to permit the following usage.
29+
'=? operator' => ['UPDATE foo SET bar=?', 'UPDATE foo SET bar=$1']
30+
];
31+
}
32+
33+
/**
34+
* Tests that operators containing the question mark character, that could be confused with unnamed parameters,
35+
* are parsed correctly.
36+
*
37+
* @see parseNamedParams
38+
* @see https://github.com/amphp/postgres/issues/39
39+
* @dataProvider provideProblematicOperators
40+
*/
41+
public function testParseProblematicOperators(string $sql): void
42+
{
43+
self::assertSame($sql, parseNamedParams($sql, $dummy));
44+
}
45+
46+
public function provideProblematicOperators(): iterable
47+
{
48+
return [
49+
// JSONB operators. https://postgresql.org/docs/12/functions-json.html#FUNCTIONS-JSONB-OP-TABLE
50+
// Bare question mark currently unsupported. ?| can be used instead.
51+
#'?' => ["SELECT WHERE '{\"foo\":null}'::jsonb ? 'foo'"],
52+
'?|' => ["SELECT WHERE '{\"foo\":null}'::jsonb ?| array['foo']"],
53+
'?&' => ["SELECT WHERE '{\"foo\":null}'::jsonb ?& array['foo']"],
54+
'@?' => ["SELECT WHERE '{\"foo\":1}'::jsonb @? '$ ? (@.foo > 0)'"],
55+
56+
// Geometric operators. https://postgresql.org/docs/12/functions-geometry.html#FUNCTIONS-GEOMETRY-OP-TABLE
57+
'?- unary' => ["SELECT WHERE ?- lseg '((-1,0),(1,0))'"],
58+
'?-' => ["SELECT WHERE point '(1,0)' ?- point '(0,0)'"],
59+
'?-|' => ["SELECT WHERE lseg '((0,0),(0,1))' ?-| lseg '((0,0),(1,0))'"],
60+
'?||' => ["SELECT WHERE lseg '((-1,0),(1,0))' ?|| lseg '((-1,2),(1,2))'"],
61+
];
62+
}
63+
}

0 commit comments

Comments
 (0)