summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS4
-rw-r--r--ext/mysqli/mysqli_api.c3
-rw-r--r--ext/mysqli/tests/bug79375.phpt172
-rw-r--r--ext/mysqlnd/mysqlnd_ps.c8
-rw-r--r--ext/mysqlnd/mysqlnd_result.c2
-rw-r--r--ext/pdo_mysql/mysql_statement.c10
-rw-r--r--ext/pdo_mysql/tests/bug79375.phpt113
-rw-r--r--ext/pdo_mysql/tests/bug_74376.phpt4
8 files changed, 309 insertions, 7 deletions
diff --git a/NEWS b/NEWS
index 8e6bde304e..c848134c7c 100644
--- a/NEWS
+++ b/NEWS
@@ -30,6 +30,10 @@ PHP NEWS
. Fixed bug #79983 (openssl_encrypt / openssl_decrypt fail with OCB mode).
(Nikita)
+- MySQLi:
+ . Fixed bug #79375 (mysqli_store_result does not report error from lock wait
+ timeout). (Kamil Tekiela, Nikita)
+
29 Oct 2020, PHP 7.4.12
- Core:
diff --git a/ext/mysqli/mysqli_api.c b/ext/mysqli/mysqli_api.c
index 0e30f5783f..e164e701b4 100644
--- a/ext/mysqli/mysqli_api.c
+++ b/ext/mysqli/mysqli_api.c
@@ -1123,7 +1123,8 @@ void mysqli_stmt_fetch_mysqlnd(INTERNAL_FUNCTION_PARAMETERS)
}
MYSQLI_FETCH_RESOURCE_STMT(stmt, mysql_stmt, MYSQLI_STATUS_VALID);
- if (FAIL == mysqlnd_stmt_fetch(stmt->stmt, &fetched_anything)) {
+ if (FAIL == mysqlnd_stmt_fetch(stmt->stmt, &fetched_anything)) {
+ MYSQLI_REPORT_STMT_ERROR(stmt->stmt);
RETURN_BOOL(FALSE);
} else if (fetched_anything == TRUE) {
RETURN_BOOL(TRUE);
diff --git a/ext/mysqli/tests/bug79375.phpt b/ext/mysqli/tests/bug79375.phpt
new file mode 100644
index 0000000000..6c6176311d
--- /dev/null
+++ b/ext/mysqli/tests/bug79375.phpt
@@ -0,0 +1,172 @@
+--TEST--
+Bug #79375: mysqli_store_result does not report error from lock wait timeout
+--SKIPIF--
+<?php
+require_once('skipif.inc');
+require_once('skipifconnectfailure.inc');
+if (!defined('MYSQLI_STORE_RESULT_COPY_DATA')) die('skip requires mysqlnd');
+?>
+--FILE--
+<?php
+
+require_once("connect.inc");
+mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
+$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+
+$mysqli->query('DROP TABLE IF EXISTS test');
+$mysqli->query('CREATE TABLE test (first int) ENGINE = InnoDB');
+$mysqli->query('INSERT INTO test VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)');
+
+function testStmtStoreResult(mysqli $mysqli, string $name) {
+ $mysqli->query("SET innodb_lock_wait_timeout = 1");
+ $mysqli->query("START TRANSACTION");
+ $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
+ echo "Running query on $name\n";
+ $stmt = $mysqli->prepare($query);
+ $stmt->execute();
+ try {
+ $stmt->store_result();
+ echo "Got {$stmt->num_rows} for $name\n";
+ } catch(mysqli_sql_exception $e) {
+ echo $e->getMessage()."\n";
+ }
+}
+function testStmtGetResult(mysqli $mysqli, string $name) {
+ $mysqli->query("SET innodb_lock_wait_timeout = 1");
+ $mysqli->query("START TRANSACTION");
+ $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
+ echo "Running query on $name\n";
+ $stmt = $mysqli->prepare($query);
+ $stmt->execute();
+ try {
+ $res = $stmt->get_result();
+ echo "Got {$res->num_rows} for $name\n";
+ } catch(mysqli_sql_exception $e) {
+ echo $e->getMessage()."\n";
+ }
+}
+function testNormalQuery(mysqli $mysqli, string $name) {
+ $mysqli->query("SET innodb_lock_wait_timeout = 1");
+ $mysqli->query("START TRANSACTION");
+ $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
+ echo "Running query on $name\n";
+ try {
+ $res = $mysqli->query($query);
+ echo "Got {$res->num_rows} for $name\n";
+ } catch(mysqli_sql_exception $e) {
+ echo $e->getMessage()."\n";
+ }
+}
+function testStmtUseResult(mysqli $mysqli, string $name) {
+ $mysqli->query("SET innodb_lock_wait_timeout = 1");
+ $mysqli->query("START TRANSACTION");
+ $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
+ echo "Running query on $name\n";
+ $stmt = $mysqli->prepare($query);
+ $stmt->execute();
+ try {
+ $stmt->fetch(); // should throw an error
+ $stmt->fetch();
+ echo "Got {$stmt->num_rows} for $name\n";
+ } catch (mysqli_sql_exception $e) {
+ echo $e->getMessage()."\n";
+ }
+}
+function testResultFetchRow(mysqli $mysqli, string $name) {
+ $mysqli->query("SET innodb_lock_wait_timeout = 1");
+ $mysqli->query("START TRANSACTION");
+ $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
+ echo "Running query on $name\n";
+ $res = $mysqli->query($query, MYSQLI_USE_RESULT);
+ try {
+ $res->fetch_row();
+ $res->fetch_row();
+ echo "Got {$res->num_rows} for $name\n";
+ } catch(mysqli_sql_exception $e) {
+ echo $e->getMessage()."\n";
+ }
+}
+
+testStmtStoreResult($mysqli, 'first connection');
+testStmtStoreResult($mysqli2, 'second connection');
+
+$mysqli->close();
+$mysqli2->close();
+
+echo "\n";
+// try it again for get_result
+$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+
+testStmtGetResult($mysqli, 'first connection');
+testStmtGetResult($mysqli2, 'second connection');
+
+$mysqli->close();
+$mysqli2->close();
+
+echo "\n";
+// try it again with unprepared query
+$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+
+testNormalQuery($mysqli, 'first connection');
+testNormalQuery($mysqli2, 'second connection');
+
+$mysqli->close();
+$mysqli2->close();
+
+echo "\n";
+// try it again with unprepared query
+$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+
+testStmtUseResult($mysqli, 'first connection');
+testStmtUseResult($mysqli2, 'second connection');
+
+$mysqli->close();
+$mysqli2->close();
+
+echo "\n";
+// try it again using fetch_row on a result object
+$mysqli = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+$mysqli2 = new my_mysqli($host, $user, $passwd, $db, $port, $socket);
+
+testResultFetchRow($mysqli, 'first connection');
+testResultFetchRow($mysqli2, 'second connection');
+
+$mysqli->close();
+$mysqli2->close();
+
+?>
+--CLEAN--
+<?php
+ require_once("clean_table.inc");
+?>
+--EXPECTF--
+Running query on first connection
+Got %d for first connection
+Running query on second connection
+Lock wait timeout exceeded; try restarting transaction
+
+Running query on first connection
+Got %d for first connection
+Running query on second connection
+Lock wait timeout exceeded; try restarting transaction
+
+Running query on first connection
+Got %d for first connection
+Running query on second connection
+Lock wait timeout exceeded; try restarting transaction
+
+Running query on first connection
+Got %d for first connection
+Running query on second connection
+Lock wait timeout exceeded; try restarting transaction
+
+Running query on first connection
+Got 1 for first connection
+Running query on second connection
+
+Warning: mysqli_result::fetch_row(): Error while reading a row in %s on line %d
+Got 0 for second connection
diff --git a/ext/mysqlnd/mysqlnd_ps.c b/ext/mysqlnd/mysqlnd_ps.c
index de4e19a402..d4d34ee500 100644
--- a/ext/mysqlnd/mysqlnd_ps.c
+++ b/ext/mysqlnd/mysqlnd_ps.c
@@ -121,9 +121,11 @@ MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s)
stmt->state = MYSQLND_STMT_USE_OR_STORE_CALLED;
} else {
COPY_CLIENT_ERROR(conn->error_info, result->stored_data->error_info);
+ COPY_CLIENT_ERROR(stmt->error_info, result->stored_data->error_info);
stmt->result->m.free_result_contents(stmt->result);
stmt->result = NULL;
stmt->state = MYSQLND_STMT_PREPARED;
+ DBG_RETURN(NULL);
}
DBG_RETURN(result);
@@ -178,7 +180,7 @@ MYSQLND_METHOD(mysqlnd_stmt, get_result)(MYSQLND_STMT * const s)
break;
}
- if ((result = result->m.store_result(result, conn, MYSQLND_STORE_PS | MYSQLND_STORE_NO_COPY))) {
+ if (result->m.store_result(result, conn, MYSQLND_STORE_PS | MYSQLND_STORE_NO_COPY)) {
UPSERT_STATUS_SET_AFFECTED_ROWS(stmt->upsert_status, result->stored_data->row_count);
stmt->state = MYSQLND_STMT_PREPARED;
result->type = MYSQLND_RES_PS_BUF;
@@ -881,7 +883,9 @@ mysqlnd_stmt_fetch_row_unbuffered(MYSQLND_RES * result, void * param, const unsi
} else if (ret == FAIL) {
if (row_packet->error_info.error_no) {
COPY_CLIENT_ERROR(conn->error_info, row_packet->error_info);
- COPY_CLIENT_ERROR(stmt->error_info, row_packet->error_info);
+ if (stmt) {
+ COPY_CLIENT_ERROR(stmt->error_info, row_packet->error_info);
+ }
}
SET_CONNECTION_STATE(&conn->state, CONN_READY);
result->unbuf->eof_reached = TRUE; /* so next time we won't get an error */
diff --git a/ext/mysqlnd/mysqlnd_result.c b/ext/mysqlnd/mysqlnd_result.c
index 69e47759ca..f253306327 100644
--- a/ext/mysqlnd/mysqlnd_result.c
+++ b/ext/mysqlnd/mysqlnd_result.c
@@ -907,7 +907,7 @@ MYSQLND_METHOD(mysqlnd_result_unbuffered, fetch_row)(MYSQLND_RES * result, void
result->memory_pool->checkpoint = checkpoint;
DBG_INF_FMT("ret=%s fetched=%u", ret == PASS? "PASS":"FAIL", *fetched_anything);
- DBG_RETURN(PASS);
+ DBG_RETURN(ret);
}
/* }}} */
diff --git a/ext/pdo_mysql/mysql_statement.c b/ext/pdo_mysql/mysql_statement.c
index 9b880ea1fa..f3811abfe6 100644
--- a/ext/pdo_mysql/mysql_statement.c
+++ b/ext/pdo_mysql/mysql_statement.c
@@ -257,7 +257,10 @@ static int pdo_mysql_stmt_execute_prepared_libmysql(pdo_stmt_t *stmt) /* {{{ */
/* if buffered, pre-fetch all the data */
if (H->buffered) {
- mysql_stmt_store_result(S->stmt);
+ if (mysql_stmt_store_result(S->stmt)) {
+ pdo_mysql_error_stmt(stmt);
+ PDO_DBG_RETURN(0);
+ }
}
}
}
@@ -300,6 +303,7 @@ static int pdo_mysql_stmt_execute_prepared_mysqlnd(pdo_stmt_t *stmt) /* {{{ */
/* if buffered, pre-fetch all the data */
if (H->buffered) {
if (mysql_stmt_store_result(S->stmt)) {
+ pdo_mysql_error_stmt(stmt);
PDO_DBG_RETURN(0);
}
}
@@ -388,7 +392,8 @@ static int pdo_mysql_stmt_next_rowset(pdo_stmt_t *stmt) /* {{{ */
/* if buffered, pre-fetch all the data */
if (H->buffered) {
if (mysql_stmt_store_result(S->stmt)) {
- PDO_DBG_RETURN(1);
+ pdo_mysql_error_stmt(stmt);
+ PDO_DBG_RETURN(0);
}
}
}
@@ -623,6 +628,7 @@ static int pdo_mysql_stmt_fetch(pdo_stmt_t *stmt, enum pdo_fetch_orientation ori
PDO_DBG_INF_FMT("stmt=%p", S->stmt);
if (S->stmt) {
if (FAIL == mysqlnd_stmt_fetch(S->stmt, &fetched_anything) || fetched_anything == FALSE) {
+ pdo_mysql_error_stmt(stmt);
PDO_DBG_RETURN(0);
}
diff --git a/ext/pdo_mysql/tests/bug79375.phpt b/ext/pdo_mysql/tests/bug79375.phpt
new file mode 100644
index 0000000000..c7905a7018
--- /dev/null
+++ b/ext/pdo_mysql/tests/bug79375.phpt
@@ -0,0 +1,113 @@
+--TEST--
+Bug #79375: mysqli_store_result does not report error from lock wait timeout
+--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');
+
+function createDB(): PDO {
+ $db = MySQLPDOTest::factory();
+ $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
+ $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false);
+ return $db;
+}
+
+$db = createDB();
+$db2 = createDB();
+$db->query('DROP TABLE IF EXISTS test');
+$db->query('CREATE TABLE test (first int) ENGINE = InnoDB');
+$db->query('INSERT INTO test VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9)');
+
+function testNormalQuery(PDO $db, string $name) {
+ $db->exec("SET innodb_lock_wait_timeout = 1");
+ $db->exec("START TRANSACTION");
+ $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
+ echo "Running query on $name\n";
+ try {
+ $stmt = $db->query($query);
+ echo "Got {$stmt->rowCount()} for $name\n";
+ } catch (PDOException $e) {
+ echo $e->getMessage()."\n";
+ }
+}
+
+function testPrepareExecute(PDO $db, string $name) {
+ $db->exec("SET innodb_lock_wait_timeout = 1");
+ $db->exec("START TRANSACTION");
+ $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
+ echo "Running query on $name\n";
+ $stmt = $db->prepare($query);
+ try {
+ $stmt->execute();
+ echo "Got {$stmt->rowCount()} for $name\n";
+ } catch (PDOException $e) {
+ echo $e->getMessage()."\n";
+ }
+}
+
+function testUnbuffered(PDO $db, string $name) {
+ $db->setAttribute(PDO::MYSQL_ATTR_USE_BUFFERED_QUERY, false);
+ $db->exec("SET innodb_lock_wait_timeout = 1");
+ $db->exec("START TRANSACTION");
+ $query = "SELECT first FROM test WHERE first = 1 FOR UPDATE";
+ echo "Running query on $name\n";
+ $stmt = $db->prepare($query);
+ $stmt->execute();
+ try {
+ $rows = $stmt->fetchAll();
+ $count = count($rows);
+ echo "Got $count for $name\n";
+ } catch (PDOException $e) {
+ echo $e->getMessage()."\n";
+ }
+}
+
+testNormalQuery($db, 'first connection');
+testNormalQuery($db2, 'second connection');
+unset($db);
+unset($db2);
+echo "\n";
+
+$db = createDB();
+$db2 = createDB();
+testPrepareExecute($db, 'first connection');
+testPrepareExecute($db2, 'second connection');
+unset($db);
+unset($db2);
+echo "\n";
+
+$db = createDB();
+$db2 = createDB();
+testUnbuffered($db, 'first connection');
+testUnbuffered($db2, 'second connection');
+unset($db);
+unset($db2);
+echo "\n";
+
+?>
+--CLEAN--
+<?php
+require __DIR__ . '/mysql_pdo_test.inc';
+MySQLPDOTest::dropTestTable();
+?>
+--EXPECT--
+Running query on first connection
+Got 1 for first connection
+Running query on second connection
+SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction
+
+Running query on first connection
+Got 1 for first connection
+Running query on second connection
+SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction
+
+Running query on first connection
+Got 1 for first connection
+Running query on second connection
+SQLSTATE[HY000]: General error: 1205 Lock wait timeout exceeded; try restarting transaction
diff --git a/ext/pdo_mysql/tests/bug_74376.phpt b/ext/pdo_mysql/tests/bug_74376.phpt
index 5abc546d15..77e0641430 100644
--- a/ext/pdo_mysql/tests/bug_74376.phpt
+++ b/ext/pdo_mysql/tests/bug_74376.phpt
@@ -23,5 +23,7 @@ $stmt = $db->query("select (select 1 union select 2)");
print "ok";
?>
---EXPECT--
+--EXPECTF--
+
+Warning: PDO::query(): SQLSTATE[21000]: Cardinality violation: 1242 Subquery returns more than 1 row in %s on line %d
ok