mirror of
https://github.com/php/php-src.git
synced 2025-08-16 05:58:45 +02:00
MySQLnd: Support cursors in store/get result
This fixes two related issues: 1. When a PS with cursor is used in store_result/get_result, perform a COM_FETCH with maximum number of rows rather than silently switching to an unbuffered result set (in the case of store_result) or erroring (in the case of get_result). In the future, we might want to make get_result unbuffered for PS with cursors, as using cursors with buffered result sets doesn't really make sense. Unlike store_result, get_result isn't very explicit about what kind of result set is desired. 2. If the client did not request a cursor, but the server reports that a cursor exists, ignore this and treat the PS as if it has no cursor (i.e. to not use COM_FETCH). It appears to be a server side bug that a cursor used inside an SP will be reported to the client, even though the client cannot use the cursor. Fixes bug #64638, bug #72862, bug #77935. Closes GH-6518.
This commit is contained in:
parent
315f3f8dc9
commit
bc166844e3
5 changed files with 230 additions and 91 deletions
6
NEWS
6
NEWS
|
@ -8,6 +8,12 @@ PHP NEWS
|
|||
- MySQLi:
|
||||
. Fixed bug #67983 (mysqlnd with MYSQLI_OPT_INT_AND_FLOAT_NATIVE fails to
|
||||
interpret bit columns). (Nikita)
|
||||
. Fixed bug #64638 (Fetching resultsets from stored procedure with cursor
|
||||
fails). (Nikita)
|
||||
. Fixed bug #72862 (segfault using prepared statements on stored procedures
|
||||
that use a cursor). (Nikita)
|
||||
. Fixed bug #77935 (Crash in mysqlnd_fetch_stmt_row_cursor when calling an SP
|
||||
with a cursor). (Nikita)
|
||||
|
||||
07 Jan 2021, PHP 7.4.14
|
||||
|
||||
|
|
38
ext/mysqli/tests/bug77935.phpt
Normal file
38
ext/mysqli/tests/bug77935.phpt
Normal file
|
@ -0,0 +1,38 @@
|
|||
--TEST--
|
||||
Bug #77935: Crash in mysqlnd_fetch_stmt_row_cursor when calling an SP with a cursor
|
||||
--SKIPIF--
|
||||
<?php
|
||||
require_once('skipif.inc');
|
||||
require_once('skipifconnectfailure.inc');
|
||||
?>
|
||||
--FILE--
|
||||
<?php
|
||||
require_once(__DIR__ . '/connect.inc');
|
||||
|
||||
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||
$db = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket);
|
||||
$db->query('DROP PROCEDURE IF EXISTS testSp');
|
||||
$db->query(<<<'SQL'
|
||||
CREATE
|
||||
PROCEDURE `testSp`()
|
||||
BEGIN
|
||||
DECLARE `cur` CURSOR FOR SELECT 1;
|
||||
OPEN `cur`;
|
||||
CLOSE `cur`;
|
||||
SELECT 1;
|
||||
END;
|
||||
SQL);
|
||||
|
||||
$stmt = $db->prepare("CALL testSp()");
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
var_dump($row);
|
||||
}
|
||||
|
||||
?>
|
||||
--EXPECT--
|
||||
array(1) {
|
||||
[1]=>
|
||||
int(1)
|
||||
}
|
|
@ -110,21 +110,7 @@ if (!function_exists('mysqli_stmt_get_result'))
|
|||
|
||||
mysqli_stmt_close($stmt);
|
||||
|
||||
if (!$stmt = mysqli_stmt_init($link))
|
||||
printf("[032] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
|
||||
|
||||
if (!mysqli_stmt_prepare($stmt, "SELECT id, label FROM test ORDER BY id LIMIT 2"))
|
||||
printf("[033] [%d] %s\n", mysqli_stmt_errno($stmt), mysqli_stmt_error($stmt));
|
||||
|
||||
if (!mysqli_stmt_execute($stmt))
|
||||
printf("[034] [%d] %s\n", mysqli_stmt_errno($stmt), mysqli_stmt_error($stmt));
|
||||
|
||||
$id = NULL;
|
||||
$label = NULL;
|
||||
if (true !== ($tmp = mysqli_stmt_bind_result($stmt, $id, $label)))
|
||||
printf("[035] Expecting boolean/true, got %s/%s\n", gettype($tmp), var_export($tmp, 1));
|
||||
|
||||
// get_result cannot be used in PS cursor mode
|
||||
// get_result can be used in PS cursor mode
|
||||
if (!$stmt = mysqli_stmt_init($link))
|
||||
printf("[030] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
|
||||
|
||||
|
@ -137,24 +123,11 @@ if (!function_exists('mysqli_stmt_get_result'))
|
|||
if (!mysqli_stmt_execute($stmt))
|
||||
printf("[033] [%d] %s\n", mysqli_stmt_errno($stmt), mysqli_stmt_error($stmt));
|
||||
|
||||
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||
try {
|
||||
$res = mysqli_stmt_get_result($stmt);
|
||||
// we expect no segfault if we try to fetch a row because get_result should throw an error or return false
|
||||
mysqli_fetch_assoc($res);
|
||||
} catch (\mysqli_sql_exception $e) {
|
||||
echo $e->getMessage() . "\n";
|
||||
$result = mysqli_stmt_get_result($stmt);
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
var_dump($row);
|
||||
}
|
||||
|
||||
try {
|
||||
$res = $stmt->get_result();
|
||||
// we expect no segfault if we try to fetch a row because get_result should throw an error or return false
|
||||
$res->fetch_assoc();
|
||||
} catch (\mysqli_sql_exception $e) {
|
||||
echo $e->getMessage() . "\n";
|
||||
}
|
||||
mysqli_report(MYSQLI_REPORT_OFF);
|
||||
|
||||
if (!$stmt = mysqli_stmt_init($link))
|
||||
printf("[034] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
|
||||
|
||||
|
@ -213,8 +186,18 @@ Warning: mysqli_stmt_fetch(): invalid object or resource mysqli_stmt
|
|||
|
||||
Warning: mysqli_stmt_get_result(): invalid object or resource mysqli_stmt
|
||||
in %s on line %d
|
||||
mysqli_stmt_get_result() cannot be used with cursors
|
||||
get_result() cannot be used with cursors
|
||||
array(2) {
|
||||
["id"]=>
|
||||
int(1)
|
||||
["label"]=>
|
||||
string(1) "a"
|
||||
}
|
||||
array(2) {
|
||||
["id"]=>
|
||||
int(2)
|
||||
["label"]=>
|
||||
string(1) "b"
|
||||
}
|
||||
[040] [2014] [Commands out of sync; you can't run this command now]
|
||||
[041] [0] []
|
||||
array(2) {
|
||||
|
|
99
ext/mysqli/tests/ps_cursor_multiple_result_sets.phpt
Normal file
99
ext/mysqli/tests/ps_cursor_multiple_result_sets.phpt
Normal file
|
@ -0,0 +1,99 @@
|
|||
--TEST--
|
||||
PS using cursor and returning multiple result sets
|
||||
--SKIPIF--
|
||||
<?php
|
||||
require_once('skipif.inc');
|
||||
require_once('skipifconnectfailure.inc');
|
||||
?>
|
||||
--FILE--
|
||||
<?php
|
||||
require_once(__DIR__ . '/connect.inc');
|
||||
|
||||
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
|
||||
$db = my_mysqli_connect($host, $user, $passwd, $db, $port, $socket);
|
||||
$db->query('DROP PROCEDURE IF EXISTS testPs');
|
||||
$db->query(<<<'SQL'
|
||||
CREATE PROCEDURE testPs() BEGIN
|
||||
DECLARE testCursor CURSOR FOR SELECT 'stuff';
|
||||
OPEN testCursor;
|
||||
CLOSE testCursor;
|
||||
SELECT 1 as a, 2 as b;
|
||||
SELECT 3 as a, 4 as b;
|
||||
END
|
||||
SQL
|
||||
);
|
||||
|
||||
echo "use_result:\n";
|
||||
$stmt = $db->prepare("call testPs()");
|
||||
$stmt->execute();
|
||||
$stmt->bind_result($v1, $v2);
|
||||
while ($stmt->fetch()) {
|
||||
var_dump($v1, $v2);
|
||||
}
|
||||
|
||||
$stmt->next_result();
|
||||
$stmt->bind_result($v1, $v2);
|
||||
while ($stmt->fetch()) {
|
||||
var_dump($v1, $v2);
|
||||
}
|
||||
$stmt->next_result();
|
||||
|
||||
echo "\nstore_result:\n";
|
||||
$stmt = $db->prepare("call testPs()");
|
||||
$stmt->execute();
|
||||
$stmt->store_result();
|
||||
$stmt->bind_result($v1, $v2);
|
||||
while ($stmt->fetch()) {
|
||||
var_dump($v1, $v2);
|
||||
}
|
||||
|
||||
$stmt->next_result();
|
||||
$stmt->store_result();
|
||||
$stmt->bind_result($v1, $v2);
|
||||
while ($stmt->fetch()) {
|
||||
var_dump($v1, $v2);
|
||||
}
|
||||
$stmt->next_result();
|
||||
|
||||
echo "\nget_result:\n";
|
||||
$stmt = $db->prepare("call testPs()");
|
||||
$stmt->execute();
|
||||
$result = $stmt->get_result();
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
var_dump($row);
|
||||
}
|
||||
|
||||
$stmt->next_result();
|
||||
$result = $stmt->get_result();
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
var_dump($row);
|
||||
}
|
||||
$stmt->next_result();
|
||||
|
||||
?>
|
||||
--EXPECT--
|
||||
use_result:
|
||||
int(1)
|
||||
int(2)
|
||||
int(3)
|
||||
int(4)
|
||||
|
||||
store_result:
|
||||
int(1)
|
||||
int(2)
|
||||
int(3)
|
||||
int(4)
|
||||
|
||||
get_result:
|
||||
array(2) {
|
||||
["a"]=>
|
||||
int(1)
|
||||
["b"]=>
|
||||
int(2)
|
||||
}
|
||||
array(2) {
|
||||
["a"]=>
|
||||
int(3)
|
||||
["b"]=>
|
||||
int(4)
|
||||
}
|
|
@ -40,6 +40,36 @@ enum_func_status mysqlnd_stmt_execute_batch_generate_request(MYSQLND_STMT * cons
|
|||
static void mysqlnd_stmt_separate_result_bind(MYSQLND_STMT * const stmt);
|
||||
static void mysqlnd_stmt_separate_one_result_bind(MYSQLND_STMT * const stmt, const unsigned int param_no);
|
||||
|
||||
static enum_func_status mysqlnd_stmt_send_cursor_fetch_command(
|
||||
const MYSQLND_STMT_DATA *stmt, unsigned max_rows)
|
||||
{
|
||||
MYSQLND_CONN_DATA *conn = stmt->conn;
|
||||
zend_uchar buf[MYSQLND_STMT_ID_LENGTH /* statement id */ + 4 /* number of rows to fetch */];
|
||||
const MYSQLND_CSTRING payload = {(const char*) buf, sizeof(buf)};
|
||||
|
||||
int4store(buf, stmt->stmt_id);
|
||||
int4store(buf + MYSQLND_STMT_ID_LENGTH, max_rows);
|
||||
|
||||
if (conn->command->stmt_fetch(conn, payload) == FAIL) {
|
||||
COPY_CLIENT_ERROR(stmt->error_info, *conn->error_info);
|
||||
return FAIL;
|
||||
}
|
||||
return PASS;
|
||||
}
|
||||
|
||||
static zend_bool mysqlnd_stmt_check_state(const MYSQLND_STMT_DATA *stmt)
|
||||
{
|
||||
const MYSQLND_CONN_DATA *conn = stmt->conn;
|
||||
if (stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE) {
|
||||
return 0;
|
||||
}
|
||||
if (stmt->cursor_exists) {
|
||||
return GET_CONNECTION_STATE(&conn->state) == CONN_READY;
|
||||
} else {
|
||||
return GET_CONNECTION_STATE(&conn->state) == CONN_FETCHING_DATA;
|
||||
}
|
||||
}
|
||||
|
||||
/* {{{ mysqlnd_stmt::store_result */
|
||||
static MYSQLND_RES *
|
||||
MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s)
|
||||
|
@ -60,14 +90,8 @@ MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s)
|
|||
DBG_RETURN(NULL);
|
||||
}
|
||||
|
||||
if (stmt->cursor_exists) {
|
||||
/* Silently convert buffered to unbuffered, for now */
|
||||
DBG_RETURN(s->m->use_result(s));
|
||||
}
|
||||
|
||||
/* Nothing to store for UPSERT/LOAD DATA*/
|
||||
if (GET_CONNECTION_STATE(&conn->state) != CONN_FETCHING_DATA || stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE)
|
||||
{
|
||||
if (!mysqlnd_stmt_check_state(stmt)) {
|
||||
SET_CLIENT_ERROR(conn->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);
|
||||
DBG_RETURN(NULL);
|
||||
}
|
||||
|
@ -78,6 +102,12 @@ MYSQLND_METHOD(mysqlnd_stmt, store_result)(MYSQLND_STMT * const s)
|
|||
SET_EMPTY_ERROR(conn->error_info);
|
||||
MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_PS_BUFFERED_SETS);
|
||||
|
||||
if (stmt->cursor_exists) {
|
||||
if (mysqlnd_stmt_send_cursor_fetch_command(stmt, -1) == FAIL) {
|
||||
DBG_RETURN(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
result = stmt->result;
|
||||
result->type = MYSQLND_RES_PS_BUF;
|
||||
/* result->m.row_decoder = php_mysqlnd_rowp_read_binary_protocol; */
|
||||
|
@ -152,19 +182,8 @@ MYSQLND_METHOD(mysqlnd_stmt, get_result)(MYSQLND_STMT * const s)
|
|||
DBG_RETURN(NULL);
|
||||
}
|
||||
|
||||
if (stmt->cursor_exists) {
|
||||
/* Prepared statement cursors are not supported as of yet */
|
||||
char * msg;
|
||||
mnd_sprintf(&msg, 0, "%s() cannot be used with cursors", get_active_function_name());
|
||||
SET_CLIENT_ERROR(stmt->error_info, CR_NOT_IMPLEMENTED, UNKNOWN_SQLSTATE, msg);
|
||||
if (msg) {
|
||||
mnd_sprintf_free(msg);
|
||||
}
|
||||
DBG_RETURN(NULL);
|
||||
}
|
||||
|
||||
/* Nothing to store for UPSERT/LOAD DATA*/
|
||||
if (GET_CONNECTION_STATE(&conn->state) != CONN_FETCHING_DATA || stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE) {
|
||||
if (!mysqlnd_stmt_check_state(stmt)) {
|
||||
SET_CLIENT_ERROR(stmt->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);
|
||||
DBG_RETURN(NULL);
|
||||
}
|
||||
|
@ -173,6 +192,12 @@ MYSQLND_METHOD(mysqlnd_stmt, get_result)(MYSQLND_STMT * const s)
|
|||
SET_EMPTY_ERROR(conn->error_info);
|
||||
MYSQLND_INC_CONN_STATISTIC(conn->stats, STAT_BUFFERED_SETS);
|
||||
|
||||
if (stmt->cursor_exists) {
|
||||
if (mysqlnd_stmt_send_cursor_fetch_command(stmt, -1) == FAIL) {
|
||||
DBG_RETURN(NULL);
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
result = conn->m->result_init(stmt->result->field_count);
|
||||
if (!result) {
|
||||
|
@ -561,28 +586,30 @@ mysqlnd_stmt_execute_parse_response(MYSQLND_STMT * const s, enum_mysqlnd_parse_e
|
|||
DBG_INF_FMT("server_status=%u cursor=%u", UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status),
|
||||
UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status) & SERVER_STATUS_CURSOR_EXISTS);
|
||||
|
||||
if (UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status) & SERVER_STATUS_CURSOR_EXISTS) {
|
||||
DBG_INF("cursor exists");
|
||||
stmt->cursor_exists = TRUE;
|
||||
SET_CONNECTION_STATE(&conn->state, CONN_READY);
|
||||
/* Only cursor read */
|
||||
stmt->default_rset_handler = s->m->use_result;
|
||||
DBG_INF("use_result");
|
||||
} else if (stmt->flags & CURSOR_TYPE_READ_ONLY) {
|
||||
DBG_INF("asked for cursor but got none");
|
||||
/*
|
||||
We have asked for CURSOR but got no cursor, because the condition
|
||||
above is not fulfilled. Then...
|
||||
if (stmt->flags & CURSOR_TYPE_READ_ONLY) {
|
||||
if (UPSERT_STATUS_GET_SERVER_STATUS(stmt->upsert_status) & SERVER_STATUS_CURSOR_EXISTS) {
|
||||
DBG_INF("cursor exists");
|
||||
stmt->cursor_exists = TRUE;
|
||||
SET_CONNECTION_STATE(&conn->state, CONN_READY);
|
||||
/* Only cursor read */
|
||||
stmt->default_rset_handler = s->m->use_result;
|
||||
DBG_INF("use_result");
|
||||
} else {
|
||||
DBG_INF("asked for cursor but got none");
|
||||
/*
|
||||
We have asked for CURSOR but got no cursor, because the condition
|
||||
above is not fulfilled. Then...
|
||||
|
||||
This is a single-row result set, a result set with no rows, EXPLAIN,
|
||||
SHOW VARIABLES, or some other command which either a) bypasses the
|
||||
cursors framework in the server and writes rows directly to the
|
||||
network or b) is more efficient if all (few) result set rows are
|
||||
precached on client and server's resources are freed.
|
||||
*/
|
||||
/* preferred is buffered read */
|
||||
stmt->default_rset_handler = s->m->store_result;
|
||||
DBG_INF("store_result");
|
||||
This is a single-row result set, a result set with no rows, EXPLAIN,
|
||||
SHOW VARIABLES, or some other command which either a) bypasses the
|
||||
cursors framework in the server and writes rows directly to the
|
||||
network or b) is more efficient if all (few) result set rows are
|
||||
precached on client and server's resources are freed.
|
||||
*/
|
||||
/* preferred is buffered read */
|
||||
stmt->default_rset_handler = s->m->store_result;
|
||||
DBG_INF("store_result");
|
||||
}
|
||||
} else {
|
||||
DBG_INF("no cursor");
|
||||
/* preferred is unbuffered read */
|
||||
|
@ -940,11 +967,7 @@ MYSQLND_METHOD(mysqlnd_stmt, use_result)(MYSQLND_STMT * s)
|
|||
}
|
||||
DBG_INF_FMT("stmt=%lu", stmt->stmt_id);
|
||||
|
||||
if (!stmt->field_count ||
|
||||
(!stmt->cursor_exists && GET_CONNECTION_STATE(&conn->state) != CONN_FETCHING_DATA) ||
|
||||
(stmt->cursor_exists && GET_CONNECTION_STATE(&conn->state) != CONN_READY) ||
|
||||
(stmt->state != MYSQLND_STMT_WAITING_USE_OR_STORE))
|
||||
{
|
||||
if (!stmt->field_count || !mysqlnd_stmt_check_state(stmt)) {
|
||||
SET_CLIENT_ERROR(conn->error_info, CR_COMMANDS_OUT_OF_SYNC, UNKNOWN_SQLSTATE, mysqlnd_out_of_sync);
|
||||
DBG_ERR("command out of sync");
|
||||
DBG_RETURN(NULL);
|
||||
|
@ -974,7 +997,6 @@ mysqlnd_fetch_stmt_row_cursor(MYSQLND_RES * result, void * param, const unsigned
|
|||
MYSQLND_STMT * s = (MYSQLND_STMT *) param;
|
||||
MYSQLND_STMT_DATA * stmt = s? s->data : NULL;
|
||||
MYSQLND_CONN_DATA * conn = stmt? stmt->conn : NULL;
|
||||
zend_uchar buf[MYSQLND_STMT_ID_LENGTH /* statement id */ + 4 /* number of rows to fetch */];
|
||||
MYSQLND_PACKET_ROW * row_packet;
|
||||
|
||||
DBG_ENTER("mysqlnd_fetch_stmt_row_cursor");
|
||||
|
@ -998,18 +1020,9 @@ mysqlnd_fetch_stmt_row_cursor(MYSQLND_RES * result, void * param, const unsigned
|
|||
SET_EMPTY_ERROR(stmt->error_info);
|
||||
SET_EMPTY_ERROR(conn->error_info);
|
||||
|
||||
int4store(buf, stmt->stmt_id);
|
||||
int4store(buf + MYSQLND_STMT_ID_LENGTH, 1); /* for now fetch only one row */
|
||||
|
||||
{
|
||||
const MYSQLND_CSTRING payload = {(const char*) buf, sizeof(buf)};
|
||||
|
||||
ret = conn->command->stmt_fetch(conn, payload);
|
||||
if (ret == FAIL) {
|
||||
COPY_CLIENT_ERROR(stmt->error_info, *conn->error_info);
|
||||
DBG_RETURN(FAIL);
|
||||
}
|
||||
|
||||
/* for now fetch only one row */
|
||||
if (mysqlnd_stmt_send_cursor_fetch_command(stmt, 1) == FAIL) {
|
||||
DBG_RETURN(FAIL);
|
||||
}
|
||||
|
||||
row_packet->skip_extraction = stmt->result_bind? FALSE:TRUE;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue