Skip to content

MySQLnd: Support cursors in store/get result #6518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions ext/mysqli/tests/bug77935.phpt
Original file line number Diff line number Diff line change
@@ -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)
}
35 changes: 16 additions & 19 deletions ext/mysqli/tests/mysqli_stmt_get_result.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ if (!function_exists('mysqli_stmt_get_result'))

mysqli_stmt_close($stmt);

// 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));

Expand All @@ -122,23 +122,10 @@ 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";
}

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";
$result = mysqli_stmt_get_result($stmt);
while ($row = mysqli_fetch_assoc($result)) {
var_dump($row);
}
mysqli_report(MYSQLI_REPORT_OFF);

if (!$stmt = mysqli_stmt_init($link))
printf("[034] [%d] %s\n", mysqli_errno($link), mysqli_error($link));
Expand Down Expand Up @@ -196,8 +183,18 @@ if (!function_exists('mysqli_stmt_get_result'))
mysqli_stmt object is not fully initialized
mysqli_stmt object is not fully initialized
mysqli_stmt object is not fully initialized
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) {
Expand Down
99 changes: 99 additions & 0 deletions ext/mysqli/tests/ps_cursor_multiple_result_sets.phpt
Original file line number Diff line number Diff line change
@@ -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)
}
131 changes: 72 additions & 59 deletions ext/mysqlnd/mysqlnd_ps.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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 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)
Expand All @@ -57,14 +87,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);
}
Expand All @@ -75,6 +99,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; */
Expand Down Expand Up @@ -127,19 +157,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);
}
Expand All @@ -148,6 +167,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) {
Expand Down Expand Up @@ -551,28 +576,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...

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");
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");
}
} else {
DBG_INF("no cursor");
/* preferred is unbuffered read */
Expand Down Expand Up @@ -718,11 +745,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);
Expand Down Expand Up @@ -752,7 +775,6 @@ mysqlnd_fetch_stmt_row_cursor(MYSQLND_RES * result, zval **row_ptr, const unsign
enum_func_status ret;
MYSQLND_STMT_DATA * stmt = result->unbuf->stmt;
MYSQLND_CONN_DATA * conn = stmt->conn;
zend_uchar buf[MYSQLND_STMT_ID_LENGTH /* statement id */ + 4 /* number of rows to fetch */];
MYSQLND_PACKET_ROW * row_packet;
void *checkpoint;

Expand All @@ -777,18 +799,9 @@ mysqlnd_fetch_stmt_row_cursor(MYSQLND_RES * result, zval **row_ptr, const unsign
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);
}

checkpoint = result->memory_pool->checkpoint;
Expand Down