diff options
author | Nikita Popov <nikita.ppv@gmail.com> | 2020-12-16 12:12:06 +0100 |
---|---|---|
committer | Nikita Popov <nikita.ppv@gmail.com> | 2020-12-18 10:19:13 +0100 |
commit | bc166844e37a6e1531a18dc0916fbe508152fc6c (patch) | |
tree | c7f9d77387bba95a1ef7feee421da07c4c104448 /ext/mysqlnd | |
parent | 315f3f8dc9b3ba3639c9284301933286fc99a825 (diff) | |
download | php-git-bc166844e37a6e1531a18dc0916fbe508152fc6c.tar.gz |
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.
Diffstat (limited to 'ext/mysqlnd')
-rw-r--r-- | ext/mysqlnd/mysqlnd_ps.c | 131 |
1 files changed, 72 insertions, 59 deletions
diff --git a/ext/mysqlnd/mysqlnd_ps.c b/ext/mysqlnd/mysqlnd_ps.c index eda9c312d5..12aace6ec2 100644 --- a/ext/mysqlnd/mysqlnd_ps.c +++ b/ext/mysqlnd/mysqlnd_ps.c @@ -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... - - 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 */ @@ -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; |