Skip to content

Commit a83cc03

Browse files
kamil-tekielanikic
authored andcommitted
Fixed bug #80458
If there is no result set (e.g. for upsert queries), still allow fetching to occur without error, i.e. treat it the same way as an empty result set. This normalizes behavior between native and emulated prepared statements and addresses a regression in PHP 7.4.13.
1 parent 8588ae7 commit a83cc03

File tree

3 files changed

+200
-11
lines changed

3 files changed

+200
-11
lines changed

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ PHP NEWS
2828
. Fixed bug #73809 (Phar Zip parse crash - mmap fail). (cmb)
2929
. Fixed #75102 (`PharData` says invalid checksum for valid tar). (cmb)
3030

31+
- PDO MySQL:
32+
. Fixed bug #80458 (PDOStatement::fetchAll() throws for upsert queries).
33+
(Kamil Tekiela)
34+
3135
- Phpdbg:
3236
. Fixed bug #76813 (Access violation near NULL on source operand). (cmb)
3337

ext/pdo_mysql/mysql_statement.c

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,12 @@ static int pdo_mysql_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_da
621621
static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori, zend_long offset) /* {{{ */
622622
{
623623
pdo_mysql_stmt *S = (pdo_mysql_stmt*)stmt->driver_data;
624-
#if PDO_USE_MYSQLND
624+
625+
if (!S->result) {
626+
PDO_DBG_RETURN(0);
627+
}
628+
629+
#ifdef PDO_USE_MYSQLND
625630
zend_bool fetched_anything;
626631

627632
PDO_DBG_ENTER("pdo_mysql_stmt_fetch");
@@ -634,6 +639,10 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori
634639

635640
PDO_DBG_RETURN(1);
636641
}
642+
643+
if (!S->stmt && S->current_data) {
644+
mnd_free(S->current_data);
645+
}
637646
#else
638647
int ret;
639648

@@ -657,16 +666,6 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori
657666
}
658667
#endif /* PDO_USE_MYSQLND */
659668

660-
if (!S->result) {
661-
strcpy(stmt->error_code, "HY000");
662-
PDO_DBG_RETURN(0);
663-
}
664-
#if PDO_USE_MYSQLND
665-
if (!S->stmt && S->current_data) {
666-
mnd_free(S->current_data);
667-
}
668-
#endif /* PDO_USE_MYSQLND */
669-
670669
if ((S->current_data = mysql_fetch_row(S->result)) == NULL) {
671670
if (!S->H->buffered && mysql_errno(S->H->server)) {
672671
pdo_mysql_error_stmt(stmt);

ext/pdo_mysql/tests/bug80458.phpt

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
--TEST--
2+
Bug #80458 PDOStatement::fetchAll() throws for upsert queries
3+
--SKIPIF--
4+
<?php
5+
if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) die('skip not loaded');
6+
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'skipif.inc');
7+
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
8+
MySQLPDOTest::skip();
9+
?>
10+
--FILE--
11+
<?php
12+
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
13+
14+
$db = MySQLPDOTest::factory();
15+
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
16+
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
17+
18+
$db->query('DROP TABLE IF EXISTS test');
19+
$db->query('CREATE TABLE test (first int) ENGINE = InnoDB');
20+
$res = $db->query('INSERT INTO test(first) VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13),(14),(15),(16)');
21+
var_dump($res->fetchAll());
22+
23+
$stmt = $db->prepare('DELETE FROM test WHERE first=1');
24+
$stmt->execute();
25+
var_dump($stmt->fetchAll());
26+
27+
$res = $db->query('DELETE FROM test WHERE first=2');
28+
var_dump($res->fetchAll());
29+
30+
$stmt2 = $db->prepare('DELETE FROM test WHERE first=3');
31+
$stmt2->execute();
32+
foreach($stmt2 as $row){
33+
// expect nothing
34+
}
35+
36+
$stmt3 = $db->prepare('DELETE FROM test WHERE first=4');
37+
$stmt3->execute();
38+
var_dump($stmt3->fetch(PDO::FETCH_ASSOC));
39+
40+
$stmt = $db->prepare('SELECT first FROM test WHERE first=5');
41+
$stmt->execute();
42+
var_dump($stmt->fetchAll());
43+
44+
$db->exec('DROP PROCEDURE IF EXISTS nores');
45+
$db->exec('CREATE PROCEDURE nores() BEGIN DELETE FROM test WHERE first=6; END;');
46+
$stmt4 = $db->prepare('CALL nores()');
47+
$stmt4->execute();
48+
var_dump($stmt4->fetchAll());
49+
$db->exec('DROP PROCEDURE IF EXISTS nores');
50+
51+
$db->exec('DROP PROCEDURE IF EXISTS ret');
52+
$db->exec('CREATE PROCEDURE ret() BEGIN SELECT first FROM test WHERE first=7; END;');
53+
$stmt5 = $db->prepare('CALL ret()');
54+
$stmt5->execute();
55+
var_dump($stmt5->fetchAll());
56+
$stmt5->nextRowset(); // needed to fetch the empty result set of CALL
57+
var_dump($stmt5->fetchAll());
58+
$db->exec('DROP PROCEDURE IF EXISTS ret');
59+
60+
/* With emulated prepares */
61+
print("Emulated prepares\n");
62+
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
63+
64+
$stmt = $db->prepare('DELETE FROM test WHERE first=8');
65+
$stmt->execute();
66+
var_dump($stmt->fetchAll());
67+
68+
$res = $db->query('DELETE FROM test WHERE first=9');
69+
var_dump($res->fetchAll());
70+
71+
$stmt2 = $db->prepare('DELETE FROM test WHERE first=10');
72+
$stmt2->execute();
73+
foreach($stmt2 as $row){
74+
// expect nothing
75+
}
76+
77+
$stmt3 = $db->prepare('DELETE FROM test WHERE first=11');
78+
$stmt3->execute();
79+
var_dump($stmt3->fetch(PDO::FETCH_ASSOC));
80+
81+
$stmt = $db->prepare('SELECT first FROM test WHERE first=12');
82+
$stmt->execute();
83+
var_dump($stmt->fetchAll());
84+
85+
$db->exec('DROP PROCEDURE IF EXISTS nores');
86+
$db->exec('CREATE PROCEDURE nores() BEGIN DELETE FROM test WHERE first=13; END;');
87+
$stmt4 = $db->prepare('CALL nores()');
88+
$stmt4->execute();
89+
var_dump($stmt4->fetchAll());
90+
$db->exec('DROP PROCEDURE IF EXISTS nores');
91+
92+
$db->exec('DROP PROCEDURE IF EXISTS ret');
93+
$db->exec('CREATE PROCEDURE ret() BEGIN SELECT first FROM test WHERE first=14; END;');
94+
$stmt5 = $db->prepare('CALL ret()');
95+
$stmt5->execute();
96+
var_dump($stmt5->fetchAll());
97+
$stmt5->nextRowset(); // needed to fetch the empty result set of CALL
98+
var_dump($stmt5->fetchAll());
99+
$db->exec('DROP PROCEDURE IF EXISTS ret');
100+
101+
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
102+
$db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
103+
104+
$stmt = $db->prepare('DELETE FROM test WHERE first=15');
105+
$stmt->execute();
106+
var_dump($stmt->fetchAll());
107+
108+
$stmt = $db->prepare('SELECT first FROM test WHERE first=16');
109+
$stmt->execute();
110+
var_dump($stmt->fetchAll());
111+
112+
?>
113+
--CLEAN--
114+
<?php
115+
require __DIR__ . '/mysql_pdo_test.inc';
116+
MySQLPDOTest::dropTestTable();
117+
?>
118+
--EXPECT--
119+
array(0) {
120+
}
121+
array(0) {
122+
}
123+
array(0) {
124+
}
125+
bool(false)
126+
array(1) {
127+
[0]=>
128+
array(2) {
129+
["first"]=>
130+
int(5)
131+
[0]=>
132+
int(5)
133+
}
134+
}
135+
array(0) {
136+
}
137+
array(1) {
138+
[0]=>
139+
array(2) {
140+
["first"]=>
141+
int(7)
142+
[0]=>
143+
int(7)
144+
}
145+
}
146+
array(0) {
147+
}
148+
Emulated prepares
149+
array(0) {
150+
}
151+
array(0) {
152+
}
153+
bool(false)
154+
array(1) {
155+
[0]=>
156+
array(2) {
157+
["first"]=>
158+
string(2) "12"
159+
[0]=>
160+
string(2) "12"
161+
}
162+
}
163+
array(0) {
164+
}
165+
array(1) {
166+
[0]=>
167+
array(2) {
168+
["first"]=>
169+
string(2) "14"
170+
[0]=>
171+
string(2) "14"
172+
}
173+
}
174+
array(0) {
175+
}
176+
array(0) {
177+
}
178+
array(1) {
179+
[0]=>
180+
array(2) {
181+
["first"]=>
182+
int(16)
183+
[0]=>
184+
int(16)
185+
}
186+
}

0 commit comments

Comments
 (0)