Commit 5ccc56b1e43 for php
commit 5ccc56b1e43fa1849a4d56fef23bf65e7b54ca7a
Author: Matthias Dötsch <matze@mdoetsch.de>
Date: Sat Oct 17 11:30:14 2026 +0100
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
diff --git a/NEWS b/NEWS
index 67c44a4420e..4a9a2135ee2 100644
--- a/NEWS
+++ b/NEWS
@@ -77,6 +77,10 @@ PHP NEWS
. Fixed bug GH-23962 (Destroying a persistent PDO instance rolls back a
transaction still in use by another instance). (Lazizbek Ergashev)
+- PDO_PGSQL:
+ . Improved performance of the SQL parser that handles placeholders.
+ (brainexe)
+
- PGSQL:
. Fixed pg_lo_write() rejecting data containing null bytes. (Ilia Alshanetsky)
diff --git a/UPGRADING b/UPGRADING
index fd3efda9df7..9442aad8450 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -134,3 +134,7 @@ PHP 8.7 UPGRADE NOTES
- MBString:
. Improved performance of mb_strlen for UTF-8 strings.
+
+- PDO_PGSQL:
+ . Improved performance of the SQL parser that handles placeholders in
+ PDO::prepare() and, with emulated prepares, PDOStatement::execute().
diff --git a/ext/pdo_pgsql/Makefile.frag b/ext/pdo_pgsql/Makefile.frag
index 3aa2761369a..2adabc51e69 100644
--- a/ext/pdo_pgsql/Makefile.frag
+++ b/ext/pdo_pgsql/Makefile.frag
@@ -1,7 +1,7 @@
$(srcdir)/pgsql_sql_parser.c: $(srcdir)/pgsql_sql_parser.re
@(cd $(top_srcdir); \
if test -f ./pgsql_sql_parser.re; then \
- $(RE2C) $(RE2C_FLAGS) -o pgsql_sql_parser.c pgsql_sql_parser.re; \
+ $(RE2C) $(RE2C_FLAGS) -b -o pgsql_sql_parser.c pgsql_sql_parser.re; \
else \
- $(RE2C) $(RE2C_FLAGS) -o ext/pdo_pgsql/pgsql_sql_parser.c ext/pdo_pgsql/pgsql_sql_parser.re; \
+ $(RE2C) $(RE2C_FLAGS) -b -o ext/pdo_pgsql/pgsql_sql_parser.c ext/pdo_pgsql/pgsql_sql_parser.re; \
fi)
diff --git a/ext/pdo_pgsql/Makefile.frag.w32 b/ext/pdo_pgsql/Makefile.frag.w32
index 0a1037d9afb..5d8047decf6 100644
--- a/ext/pdo_pgsql/Makefile.frag.w32
+++ b/ext/pdo_pgsql/Makefile.frag.w32
@@ -1,3 +1,3 @@
ext\pdo_pgsql\pgsql_sql_parser.c: ext\pdo_pgsql\pgsql_sql_parser.re
cd $(PHP_SRC_DIR)
- $(RE2C) $(RE2C_FLAGS) -o ext/pdo_pgsql/pgsql_sql_parser.c ext/pdo_pgsql/pgsql_sql_parser.re
+ $(RE2C) $(RE2C_FLAGS) -b -o ext/pdo_pgsql/pgsql_sql_parser.c ext/pdo_pgsql/pgsql_sql_parser.re
diff --git a/ext/pdo_pgsql/pgsql_sql_parser.re b/ext/pdo_pgsql/pgsql_sql_parser.re
index 011fce56a1a..ce89c32f535 100644
--- a/ext/pdo_pgsql/pgsql_sql_parser.re
+++ b/ext/pdo_pgsql/pgsql_sql_parser.re
@@ -32,6 +32,7 @@ int pdo_pgsql_scanner(pdo_scanner_t *s)
SPECIALS = [$eE:?"'/-];
MULTICHAR = [:]{2,};
ANYNOEOF = [\001-\377];
+ TEXTCHR = ANYNOEOF\SPECIALS;
*/
/*!re2c
@@ -45,6 +46,7 @@ int pdo_pgsql_scanner(pdo_scanner_t *s)
QUESTION { RET(PDO_PARSER_BIND_POS); }
SPECIALS { SKIP_ONE(PDO_PARSER_TEXT); }
COMMENTS { RET(PDO_PARSER_TEXT); }
- (ANYNOEOF\SPECIALS)+ { RET(PDO_PARSER_TEXT); }
+ /* Plain text may contain e/E, as long as it doesn't end with one, which could start an E'...' string */
+ ([eE]* TEXTCHR)+ { RET(PDO_PARSER_TEXT); }
*/
}
diff --git a/ext/pdo_pgsql/tests/pdo_pgsql_parser_e_strings.phpt b/ext/pdo_pgsql/tests/pdo_pgsql_parser_e_strings.phpt
new file mode 100644
index 00000000000..359c6e5a59f
--- /dev/null
+++ b/ext/pdo_pgsql/tests/pdo_pgsql_parser_e_strings.phpt
@@ -0,0 +1,52 @@
+--TEST--
+PgSQL PDO Parser: E'' strings next to text containing e/E
+--EXTENSIONS--
+pdo_pgsql
+--SKIPIF--
+<?php
+require __DIR__ . '/config.inc';
+require dirname(__DIR__, 2) . '/pdo/tests/pdo_test.inc';
+PDOTest::skip();
+?>
+--FILE--
+<?php
+require dirname(__DIR__, 2) . '/pdo/tests/pdo_test.inc';
+$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
+$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+$db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
+
+$queries = [
+ // The "?" and ":name" inside the E'' strings must not be treated as placeholders
+ ["SELECT E'here\\'s a ?' AS selected_e, ? AS value_e", ['one']],
+ ["SELECT 'x'||e'\\'?' AS concat_e, :name AS reference_e", ['name' => 'two']],
+ ["SELECT ('a%' LIKE 'a!%' ESCAPE'!')::text AS escape_e, E'\\\\:e' AS e, :e AS ee", ['e' => 'three']],
+ ["SELECT date'2026-10-14' AS date_e, ?::text AS where_else", ['four']],
+ ["SELECT ? AS e, ? AS E2 WHERE TRUE", ['five', 'six']],
+ ["SELECT /* e'? */ :value AS value_e -- e'?", ['value' => 'seven']],
+];
+
+foreach ([true, false] as $emulate) {
+ $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, $emulate);
+ echo $emulate ? "emulated:\n" : "native:\n";
+ foreach ($queries as [$sql, $params]) {
+ $stmt = $db->prepare($sql);
+ $stmt->execute($params);
+ echo json_encode($stmt->fetch()), "\n";
+ }
+}
+?>
+--EXPECT--
+emulated:
+{"selected_e":"here's a ?","value_e":"one"}
+{"concat_e":"x'?","reference_e":"two"}
+{"escape_e":"true","e":"\\:e","ee":"three"}
+{"date_e":"2026-10-14","where_else":"four"}
+{"e":"five","e2":"six"}
+{"value_e":"seven"}
+native:
+{"selected_e":"here's a ?","value_e":"one"}
+{"concat_e":"x'?","reference_e":"two"}
+{"escape_e":"true","e":"\\:e","ee":"three"}
+{"date_e":"2026-10-14","where_else":"four"}
+{"e":"five","e2":"six"}
+{"value_e":"seven"}