Skip to content

Commit f3607f1

Browse files
committed
Merge branch 'PHP-8.4' into PHP-8.5
* PHP-8.4: ext/pdo_pgsql: Fix CURSOR_SCROLL statements closing a nonexistent cursor # Conflicts: # ext/pdo_pgsql/pgsql_statement.c # ext/pdo_pgsql/php_pdo_pgsql_int.h
2 parents 18cd150 + c1a7ab0 commit f3607f1

10 files changed

Lines changed: 298 additions & 9 deletions

ext/pdo_pgsql/config.m4

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ if test "$PHP_PDO_PGSQL" != "no"; then
2525
or later).])],,
2626
[$PGSQL_LIBS])
2727

28+
PHP_CHECK_LIBRARY([pq], [PQclosePortal],
29+
[AC_DEFINE([HAVE_PQCLOSEPORTAL], [1],
30+
[Define to 1 if libpq has the 'PQclosePortal' function (PostgreSQL 17
31+
or later).])],,
32+
[$PGSQL_LIBS])
33+
2834
PHP_CHECK_PDO_INCLUDES
2935

3036
PHP_NEW_EXTENSION([pdo_pgsql],

ext/pdo_pgsql/config.w32

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ if (PHP_PDO_PGSQL != "no") {
99

1010
AC_DEFINE('HAVE_PDO_PGSQL', 1, "Define to 1 if the PHP extension 'pdo_pgsql' is available.");
1111

12+
if (GREP_HEADER("libpq-fe.h", "PQclosePortal", PHP_PDO_PGSQL + "\\include;" + PHP_PHP_BUILD + "\\include\\pgsql;" + PHP_PHP_BUILD + "\\include\\libpq;")) {
13+
AC_DEFINE('HAVE_PQCLOSEPORTAL', 1, "Define to 1 if libpq has the 'PQclosePortal' function (PostgreSQL 17 or later).");
14+
}
15+
1216
ADD_EXTENSION_DEP('pdo_pgsql', 'pdo');
1317
ADD_MAKEFILE_FRAGMENT();
1418
} else {

ext/pdo_pgsql/pgsql_statement.c

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,47 @@
6060
#define FIN_CLOSE 0x2
6161
#define FIN_ABORT 0x4
6262

63+
#ifndef HAVE_PQCLOSEPORTAL
64+
static bool pdo_pgsql_try_cmd(const char *cmd, const char *ok_sqlstate, pdo_pgsql_db_handle *H)
65+
{
66+
bool result = false;
67+
char *q = NULL;
68+
PGresult *res = NULL;
69+
70+
PGTransactionStatusType status = PQtransactionStatus(H->server);
71+
72+
switch (status) {
73+
case PQTRANS_ACTIVE:
74+
case PQTRANS_INERROR:
75+
break;
76+
case PQTRANS_INTRANS: /* failure must not abort the caller's transaction */
77+
/* PQexec does not run the statements following a failed one */
78+
spprintf(&q, 0, "SAVEPOINT pdo_pgsql_savepoint; %s; RELEASE SAVEPOINT pdo_pgsql_savepoint;", cmd);
79+
res = PQexec(H->server, q);
80+
81+
if (PQresultStatus(res) != PGRES_COMMAND_OK) {
82+
PQclear(PQexec(H->server, "ROLLBACK TO SAVEPOINT pdo_pgsql_savepoint; RELEASE SAVEPOINT pdo_pgsql_savepoint"));
83+
}
84+
85+
break;
86+
default:
87+
res = PQexec(H->server, cmd);
88+
}
89+
90+
if (PQresultStatus(res) == PGRES_COMMAND_OK) {
91+
result = true;
92+
} else if (res) {
93+
const char *sqlstate = pdo_pgsql_sqlstate(res);
94+
95+
result = sqlstate && !strcmp(sqlstate, ok_sqlstate);
96+
}
97+
98+
if (q) efree(q);
99+
if (res) PQclear(res);
100+
101+
return result;
102+
}
103+
#endif
63104

64105

65106
static void pgsql_stmt_finish(pdo_pgsql_stmt *S, int fin_mode)
@@ -180,15 +221,16 @@ static int pgsql_stmt_dtor(pdo_stmt_t *stmt)
180221
}
181222

182223
if (S->cursor_name) {
183-
if (server_obj_usable) {
224+
if (S->is_cursor_declared && server_obj_usable) {
184225
pdo_pgsql_db_handle *H = S->H;
185-
char *q = NULL;
186-
PGresult *res;
187-
226+
#ifndef HAVE_PQCLOSEPORTAL
227+
char *q;
188228
spprintf(&q, 0, "CLOSE %s", S->cursor_name);
189-
res = PQexec(H->server, q);
229+
pdo_pgsql_try_cmd(q, "34000", H); /* 34000: invalid_cursor_name */
190230
efree(q);
191-
if (res) PQclear(res);
231+
#else
232+
PQclear(PQclosePortal(H->server, S->cursor_name));
233+
#endif
192234
}
193235
efree(S->cursor_name);
194236
S->cursor_name = NULL;
@@ -228,10 +270,25 @@ static int pgsql_stmt_execute(pdo_stmt_t *stmt)
228270
if (S->cursor_name) {
229271
char *q = NULL;
230272

231-
if (S->is_prepared) {
273+
if (S->is_cursor_declared) {
274+
#ifndef HAVE_PQCLOSEPORTAL
232275
spprintf(&q, 0, "CLOSE %s", S->cursor_name);
233-
PQclear(PQexec(H->server, q));
276+
277+
/* 34000: invalid_cursor_name */
278+
if (pdo_pgsql_try_cmd(q, "34000", H)) {
279+
S->is_cursor_declared = false;
280+
}
281+
234282
efree(q);
283+
#else
284+
PGresult *res = PQclosePortal(H->server, S->cursor_name);
285+
286+
if (PQresultStatus(res) == PGRES_COMMAND_OK) {
287+
S->is_cursor_declared = false;
288+
}
289+
290+
PQclear(res);
291+
#endif
235292
}
236293

237294
spprintf(&q, 0, "DECLARE %s SCROLL CURSOR WITH HOLD FOR %s", S->cursor_name, ZSTR_VAL(stmt->active_query_string));
@@ -247,7 +304,7 @@ static int pgsql_stmt_execute(pdo_stmt_t *stmt)
247304
PQclear(S->result);
248305

249306
/* the cursor was declared correctly */
250-
S->is_prepared = 1;
307+
S->is_cursor_declared = true;
251308

252309
/* fetch to be able to get the number of tuples later, but don't advance the cursor pointer */
253310
spprintf(&q, 0, "FETCH FORWARD 0 FROM %s", S->cursor_name);

ext/pdo_pgsql/php_pdo_pgsql_int.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ struct pdo_pgsql_stmt {
6969
Oid *param_types;
7070
int current_row;
7171
bool is_prepared;
72+
bool is_cursor_declared;
7273
bool is_unbuffered;
7374
bool is_running_unbuffered;
7475
};
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
--TEST--
2+
PDO PgSQL PDO::CURSOR_SCROLL keeps track of a held cursor when the CLOSE before a re-declare fails
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+
14+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
15+
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
16+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
17+
18+
$stmt = $db->prepare('SELECT CAST(:v AS int)', [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]);
19+
$stmt->execute([':v' => '1']);
20+
21+
$db->beginTransaction();
22+
23+
try {
24+
$db->exec('SELECT 1 / 0');
25+
} catch (PDOException $e) {
26+
echo $e::class, ': ', $e->getCode(), PHP_EOL;
27+
}
28+
29+
try {
30+
$stmt->execute([':v' => '2']);
31+
} catch (PDOException $e) {
32+
echo $e::class, ': ', $e->getCode(), PHP_EOL;
33+
}
34+
35+
$db->rollBack();
36+
unset($stmt);
37+
38+
var_dump($db->query("SELECT count(*) FROM pg_cursors WHERE name LIKE 'pdo\_crsr\_%'")->fetchColumn());
39+
40+
?>
41+
--EXPECT--
42+
PDOException: 22012
43+
PDOException: 25P02
44+
string(1) "0"
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
PDO PgSQL PDO::CURSOR_SCROLL cursor destroyed by DISCARD ALL does not break the transaction
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+
14+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
15+
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
16+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
17+
18+
$stmt = $db->prepare('SELECT 1', [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]);
19+
$stmt->execute();
20+
21+
/* a connection pooler issues this when handing the connection back */
22+
$db->exec('DISCARD ALL');
23+
24+
$db->beginTransaction();
25+
26+
unset($stmt);
27+
28+
echo $db->query('SELECT 2')->fetchColumn(), PHP_EOL;
29+
30+
$db->rollBack();
31+
32+
echo 'Done', PHP_EOL;
33+
34+
?>
35+
--EXPECT--
36+
2
37+
Done
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
--TEST--
2+
PDO PgSQL PDO::CURSOR_SCROLL sends no CLOSE after a failed re-declare
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+
14+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
15+
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
16+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
17+
18+
$stmt = $db->prepare('SELECT CAST(:v AS int)', [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]);
19+
$stmt->execute([':v' => '1']);
20+
21+
try {
22+
$stmt->execute([':v' => 'not an int']);
23+
} catch (PDOException $e) {
24+
echo $e::class, ': ', $e->getCode(), PHP_EOL;
25+
}
26+
27+
$db->beginTransaction();
28+
unset($stmt);
29+
30+
$db->exec('SELECT 2');
31+
32+
echo 'Done', PHP_EOL;
33+
34+
?>
35+
--EXPECT--
36+
PDOException: 22P02
37+
Done
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
PDO PgSQL PDO::CURSOR_SCROLL re-execute after a rollback destroyed the cursor
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+
14+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
15+
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
16+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
17+
18+
$db->beginTransaction();
19+
20+
$stmt = $db->prepare('SELECT 1', [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]);
21+
$stmt->execute();
22+
23+
$db->rollBack();
24+
25+
$db->beginTransaction();
26+
27+
$stmt->execute();
28+
echo $stmt->fetchColumn(), PHP_EOL;
29+
30+
echo $db->query('SELECT 2')->fetchColumn(), PHP_EOL;
31+
32+
$db->rollBack();
33+
34+
echo 'Done', PHP_EOL;
35+
36+
?>
37+
--EXPECT--
38+
1
39+
2
40+
Done
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
--TEST--
2+
PDO PgSQL PDO::CURSOR_SCROLL sends no CLOSE for a cursor a rollback already destroyed
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+
14+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
15+
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
16+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
17+
18+
$db->beginTransaction();
19+
20+
$stmt = $db->prepare('SELECT 1', [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]);
21+
$stmt->execute();
22+
23+
$db->rollBack();
24+
25+
$db->beginTransaction();
26+
unset($stmt);
27+
28+
$db->exec('SELECT 2');
29+
30+
echo 'Done', PHP_EOL;
31+
32+
?>
33+
--EXPECT--
34+
Done
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
--TEST--
2+
PDO PgSQL PDO::CURSOR_SCROLL sends no CLOSE for a cursor it never declared
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+
14+
require __DIR__ . '/../../../ext/pdo/tests/pdo_test.inc';
15+
$db = PDOTest::test_factory(__DIR__ . '/common.phpt');
16+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
17+
18+
$db->beginTransaction();
19+
20+
$stmt = $db->prepare('SELECT 1', [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL]);
21+
unset($stmt);
22+
23+
$db->exec('SELECT 2');
24+
25+
echo 'Done';
26+
27+
?>
28+
--EXPECT--
29+
Done

0 commit comments

Comments
 (0)