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.
This commit is contained in:
Dharman 2020-12-02 21:24:20 +00:00 committed by Nikita Popov
parent 8588ae7215
commit a83cc03c13
3 changed files with 200 additions and 11 deletions

4
NEWS
View File

@ -28,6 +28,10 @@ PHP NEWS
. Fixed bug #73809 (Phar Zip parse crash - mmap fail). (cmb)
. Fixed #75102 (`PharData` says invalid checksum for valid tar). (cmb)
- PDO MySQL:
. Fixed bug #80458 (PDOStatement::fetchAll() throws for upsert queries).
(Kamil Tekiela)
- Phpdbg:
. Fixed bug #76813 (Access violation near NULL on source operand). (cmb)

View File

@ -621,7 +621,12 @@ static int pdo_mysql_stmt_param_hook(pdo_stmt_t *stmt, struct pdo_bound_param_da
static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori, zend_long offset) /* {{{ */
{
pdo_mysql_stmt *S = (pdo_mysql_stmt*)stmt->driver_data;
#if PDO_USE_MYSQLND
if (!S->result) {
PDO_DBG_RETURN(0);
}
#ifdef PDO_USE_MYSQLND
zend_bool fetched_anything;
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
PDO_DBG_RETURN(1);
}
if (!S->stmt && S->current_data) {
mnd_free(S->current_data);
}
#else
int ret;
@ -657,16 +666,6 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori
}
#endif /* PDO_USE_MYSQLND */
if (!S->result) {
strcpy(stmt->error_code, "HY000");
PDO_DBG_RETURN(0);
}
#if PDO_USE_MYSQLND
if (!S->stmt && S->current_data) {
mnd_free(S->current_data);
}
#endif /* PDO_USE_MYSQLND */
if ((S->current_data = mysql_fetch_row(S->result)) == NULL) {
if (!S->H->buffered && mysql_errno(S->H->server)) {
pdo_mysql_error_stmt(stmt);

View File

@ -0,0 +1,186 @@
--TEST--
Bug #80458 PDOStatement::fetchAll() throws for upsert queries
--SKIPIF--
<?php
if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) die('skip not loaded');
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'skipif.inc');
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
MySQLPDOTest::skip();
?>
--FILE--
<?php
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'mysql_pdo_test.inc');
$db = MySQLPDOTest::factory();
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$db->query('DROP TABLE IF EXISTS test');
$db->query('CREATE TABLE test (first int) ENGINE = InnoDB');
$res = $db->query('INSERT INTO test(first) VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12),(13),(14),(15),(16)');
var_dump($res->fetchAll());
$stmt = $db->prepare('DELETE FROM test WHERE first=1');
$stmt->execute();
var_dump($stmt->fetchAll());
$res = $db->query('DELETE FROM test WHERE first=2');
var_dump($res->fetchAll());
$stmt2 = $db->prepare('DELETE FROM test WHERE first=3');
$stmt2->execute();
foreach($stmt2 as $row){
// expect nothing
}
$stmt3 = $db->prepare('DELETE FROM test WHERE first=4');
$stmt3->execute();
var_dump($stmt3->fetch(PDO::FETCH_ASSOC));
$stmt = $db->prepare('SELECT first FROM test WHERE first=5');
$stmt->execute();
var_dump($stmt->fetchAll());
$db->exec('DROP PROCEDURE IF EXISTS nores');
$db->exec('CREATE PROCEDURE nores() BEGIN DELETE FROM test WHERE first=6; END;');
$stmt4 = $db->prepare('CALL nores()');
$stmt4->execute();
var_dump($stmt4->fetchAll());
$db->exec('DROP PROCEDURE IF EXISTS nores');
$db->exec('DROP PROCEDURE IF EXISTS ret');
$db->exec('CREATE PROCEDURE ret() BEGIN SELECT first FROM test WHERE first=7; END;');
$stmt5 = $db->prepare('CALL ret()');
$stmt5->execute();
var_dump($stmt5->fetchAll());
$stmt5->nextRowset(); // needed to fetch the empty result set of CALL
var_dump($stmt5->fetchAll());
$db->exec('DROP PROCEDURE IF EXISTS ret');
/* With emulated prepares */
print("Emulated prepares\n");
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
$stmt = $db->prepare('DELETE FROM test WHERE first=8');
$stmt->execute();
var_dump($stmt->fetchAll());
$res = $db->query('DELETE FROM test WHERE first=9');
var_dump($res->fetchAll());
$stmt2 = $db->prepare('DELETE FROM test WHERE first=10');
$stmt2->execute();
foreach($stmt2 as $row){
// expect nothing
}
$stmt3 = $db->prepare('DELETE FROM test WHERE first=11');
$stmt3->execute();
var_dump($stmt3->fetch(PDO::FETCH_ASSOC));
$stmt = $db->prepare('SELECT first FROM test WHERE first=12');
$stmt->execute();
var_dump($stmt->fetchAll());
$db->exec('DROP PROCEDURE IF EXISTS nores');
$db->exec('CREATE PROCEDURE nores() BEGIN DELETE FROM test WHERE first=13; END;');
$stmt4 = $db->prepare('CALL nores()');
$stmt4->execute();
var_dump($stmt4->fetchAll());
$db->exec('DROP PROCEDURE IF EXISTS nores');
$db->exec('DROP PROCEDURE IF EXISTS ret');
$db->exec('CREATE PROCEDURE ret() BEGIN SELECT first FROM test WHERE first=14; END;');
$stmt5 = $db->prepare('CALL ret()');
$stmt5->execute();
var_dump($stmt5->fetchAll());
$stmt5->nextRowset(); // needed to fetch the empty result set of CALL
var_dump($stmt5->fetchAll());
$db->exec('DROP PROCEDURE IF EXISTS ret');
$db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
$db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
$stmt = $db->prepare('DELETE FROM test WHERE first=15');
$stmt->execute();
var_dump($stmt->fetchAll());
$stmt = $db->prepare('SELECT first FROM test WHERE first=16');
$stmt->execute();
var_dump($stmt->fetchAll());
?>
--CLEAN--
<?php
require __DIR__ . '/mysql_pdo_test.inc';
MySQLPDOTest::dropTestTable();
?>
--EXPECT--
array(0) {
}
array(0) {
}
array(0) {
}
bool(false)
array(1) {
[0]=>
array(2) {
["first"]=>
int(5)
[0]=>
int(5)
}
}
array(0) {
}
array(1) {
[0]=>
array(2) {
["first"]=>
int(7)
[0]=>
int(7)
}
}
array(0) {
}
Emulated prepares
array(0) {
}
array(0) {
}
bool(false)
array(1) {
[0]=>
array(2) {
["first"]=>
string(2) "12"
[0]=>
string(2) "12"
}
}
array(0) {
}
array(1) {
[0]=>
array(2) {
["first"]=>
string(2) "14"
[0]=>
string(2) "14"
}
}
array(0) {
}
array(0) {
}
array(1) {
[0]=>
array(2) {
["first"]=>
int(16)
[0]=>
int(16)
}
}