Skip to content

Commit 5ccc56b

Browse files
brainexedevnexen
authored andcommitted
ext/pdo_pgsql: Speed up the SQL scanner
The scanner that finds placeholders in queries excluded e/E from plain text, because they can start an E'...' string. So every e/E in a query (SELECT, WHERE, created_at, ...) ended a text token and became a one-character token of its own. Allow e/E inside text as long as the text doesn't end with one, so E'...' strings are still recognized. Also let re2c generate bitmaps (-b) for the character classes instead of a switch on every input character. The scanner runs in PDO::prepare() and, with emulated prepares, again in PDOStatement::execute(). Instructions for 200k calls of prepare(): emulated native SELECT, 11 cols -24% -16% INSERT, 8 params -15% -7% For a whole emulated query (prepare, execute, fetch one row) against a local server, PHP executes 7-9% fewer instructions. Co-authored-by: Gina Peter Banyard <girgias@php.net> Close GH-24353
1 parent 070ce2f commit 5ccc56b

6 files changed

Lines changed: 66 additions & 4 deletions

File tree

‎NEWS‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ PHP NEWS
7777
. Fixed bug GH-23962 (Destroying a persistent PDO instance rolls back a
7878
transaction still in use by another instance). (Lazizbek Ergashev)
7979

80+
- PDO_PGSQL:
81+
. Improved performance of the SQL parser that handles placeholders.
82+
(brainexe)
83+
8084
- PGSQL:
8185
. Fixed pg_lo_write() rejecting data containing null bytes. (Ilia Alshanetsky)
8286

‎UPGRADING‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,3 +134,7 @@ PHP 8.7 UPGRADE NOTES
134134

135135
- MBString:
136136
. Improved performance of mb_strlen for UTF-8 strings.
137+
138+
- PDO_PGSQL:
139+
. Improved performance of the SQL parser that handles placeholders in
140+
PDO::prepare() and, with emulated prepares, PDOStatement::execute().

‎ext/pdo_pgsql/Makefile.frag‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
$(srcdir)/pgsql_sql_parser.c: $(srcdir)/pgsql_sql_parser.re
22
@(cd $(top_srcdir); \
33
if test -f ./pgsql_sql_parser.re; then \
4-
$(RE2C) $(RE2C_FLAGS) -o pgsql_sql_parser.c pgsql_sql_parser.re; \
4+
$(RE2C) $(RE2C_FLAGS) -b -o pgsql_sql_parser.c pgsql_sql_parser.re; \
55
else \
6-
$(RE2C) $(RE2C_FLAGS) -o ext/pdo_pgsql/pgsql_sql_parser.c ext/pdo_pgsql/pgsql_sql_parser.re; \
6+
$(RE2C) $(RE2C_FLAGS) -b -o ext/pdo_pgsql/pgsql_sql_parser.c ext/pdo_pgsql/pgsql_sql_parser.re; \
77
fi)

‎ext/pdo_pgsql/Makefile.frag.w32‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
ext\pdo_pgsql\pgsql_sql_parser.c: ext\pdo_pgsql\pgsql_sql_parser.re
22
cd $(PHP_SRC_DIR)
3-
$(RE2C) $(RE2C_FLAGS) -o ext/pdo_pgsql/pgsql_sql_parser.c ext/pdo_pgsql/pgsql_sql_parser.re
3+
$(RE2C) $(RE2C_FLAGS) -b -o ext/pdo_pgsql/pgsql_sql_parser.c ext/pdo_pgsql/pgsql_sql_parser.re

‎ext/pdo_pgsql/pgsql_sql_parser.re‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ int pdo_pgsql_scanner(pdo_scanner_t *s)
3232
SPECIALS = [$eE:?"'/-];
3333
MULTICHAR = [:]{2,};
3434
ANYNOEOF = [\001-\377];
35+
TEXTCHR = ANYNOEOF\SPECIALS;
3536
*/
3637
3738
/*!re2c
@@ -45,6 +46,7 @@ int pdo_pgsql_scanner(pdo_scanner_t *s)
4546
QUESTION { RET(PDO_PARSER_BIND_POS); }
4647
SPECIALS { SKIP_ONE(PDO_PARSER_TEXT); }
4748
COMMENTS { RET(PDO_PARSER_TEXT); }
48-
(ANYNOEOF\SPECIALS)+ { RET(PDO_PARSER_TEXT); }
49+
/* Plain text may contain e/E, as long as it doesn't end with one, which could start an E'...' string */
50+
([eE]* TEXTCHR)+ { RET(PDO_PARSER_TEXT); }
4951
*/
5052
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
--TEST--
2+
PgSQL PDO Parser: E'' strings next to text containing e/E
3+
--EXTENSIONS--
4+
pdo_pgsql
5+
--SKIPIF--
6+
<?php
7+
require __DIR__ . '/config.inc';
8+
require dirname(__DIR__, 2) . '/pdo/tests/pdo_test.inc';
9+
PDOTest::skip();
10+
?>
11+
--FILE--
12+
<?php
13+
require dirname(__DIR__, 2) . '/pdo/tests/pdo_test.inc';
14+
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
15+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
16+
$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
17+
18+
$queries = [
19+
// The "?" and ":name" inside the E'' strings must not be treated as placeholders
20+
["SELECT E'here\\'s a ?' AS selected_e, ? AS value_e", ['one']],
21+
["SELECT 'x'||e'\\'?' AS concat_e, :name AS reference_e", ['name' => 'two']],
22+
["SELECT ('a%' LIKE 'a!%' ESCAPE'!')::text AS escape_e, E'\\\\:e' AS e, :e AS ee", ['e' => 'three']],
23+
["SELECT date'2026-10-14' AS date_e, ?::text AS where_else", ['four']],
24+
["SELECT ? AS e, ? AS E2 WHERE TRUE", ['five', 'six']],
25+
["SELECT /* e'? */ :value AS value_e -- e'?", ['value' => 'seven']],
26+
];
27+
28+
foreach ([true, false] as $emulate) {
29+
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate);
30+
echo $emulate ? "emulated:\n" : "native:\n";
31+
foreach ($queries as [$sql, $params]) {
32+
$stmt = $db->prepare($sql);
33+
$stmt->execute($params);
34+
echo json_encode($stmt->fetch()), "\n";
35+
}
36+
}
37+
?>
38+
--EXPECT--
39+
emulated:
40+
{"selected_e":"here's a ?","value_e":"one"}
41+
{"concat_e":"x'?","reference_e":"two"}
42+
{"escape_e":"true","e":"\\:e","ee":"three"}
43+
{"date_e":"2026-10-14","where_else":"four"}
44+
{"e":"five","e2":"six"}
45+
{"value_e":"seven"}
46+
native:
47+
{"selected_e":"here's a ?","value_e":"one"}
48+
{"concat_e":"x'?","reference_e":"two"}
49+
{"escape_e":"true","e":"\\:e","ee":"three"}
50+
{"date_e":"2026-10-14","where_else":"four"}
51+
{"e":"five","e2":"six"}
52+
{"value_e":"seven"}

0 commit comments

Comments
 (0)