Skip to content

Commit c417153

Browse files
amitabnmariz
authored andcommitted
BUG26376334: Fix prepared statements in MySQL 8.0
In MySQL 8.0, all prepared statements hang on calling `fetchall` on a cursor. This happens because the server expects a CMD_STMT_FETCH command from the client to send the results of the prepared query. This patch adds new flags to the `MySQLCursorPrepared` class to check for `SERVER_STATUS_CURSOR_EXISTS` and `SERVER_STATUS_LAST_ROW_SENT` flags in the EOF/OK packets and then send `CMD_STMT_FETCH` command accordingly. Tests failing with MySQL 8.0 will now pass with this patch.
1 parent 13e9425 commit c417153

File tree

4 files changed

+94
-12
lines changed

4 files changed

+94
-12
lines changed

lib/mysql/connector/connection.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,17 @@ def _handle_binary_result(self, packet):
933933
eof = self._handle_eof(self._socket.recv())
934934
return (column_count, columns, eof)
935935

936+
def cmd_stmt_fetch(self, statement_id, rows=1):
937+
"""Fetch a MySQL statement Result Set
938+
939+
This method will send the FETCH command to MySQL together with the
940+
given statement id and the number of rows to fetch.
941+
"""
942+
packet = self._protocol.make_stmt_fetch(statement_id, rows)
943+
self.unread_result = False
944+
self._send_cmd(ServerCmd.STMT_FETCH, packet, expect_response=False)
945+
self.unread_result = True
946+
936947
def cmd_stmt_prepare(self, statement):
937948
"""Prepare a MySQL statement
938949

lib/mysql/connector/constants.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -472,6 +472,11 @@ class ServerFlag(_Flags):
472472
STATUS_LAST_ROW_SENT = 1 << 7
473473
STATUS_DB_DROPPED = 1 << 8
474474
STATUS_NO_BACKSLASH_ESCAPES = 1 << 9
475+
SERVER_STATUS_METADATA_CHANGED = 1 << 10
476+
SERVER_QUERY_WAS_SLOW = 1 << 11
477+
SERVER_PS_OUT_PARAMS = 1 << 12
478+
SERVER_STATUS_IN_TRANS_READONLY = 1 << 13
479+
SERVER_SESSION_STATE_CHANGED = 1 << 14
475480

476481
desc = {
477482
'SERVER_STATUS_IN_TRANS': (1 << 0,
@@ -483,10 +488,31 @@ class ServerFlag(_Flags):
483488
'next query exists'),
484489
'SERVER_QUERY_NO_GOOD_INDEX_USED': (1 << 4, ''),
485490
'SERVER_QUERY_NO_INDEX_USED': (1 << 5, ''),
486-
'SERVER_STATUS_CURSOR_EXISTS': (1 << 6, ''),
487-
'SERVER_STATUS_LAST_ROW_SENT': (1 << 7, ''),
491+
'SERVER_STATUS_CURSOR_EXISTS': (1 << 6,
492+
'Set when server opened a read-only '
493+
'non-scrollable cursor for a query.'),
494+
'SERVER_STATUS_LAST_ROW_SENT': (1 << 7,
495+
'Set when a read-only cursor is '
496+
'exhausted'),
488497
'SERVER_STATUS_DB_DROPPED': (1 << 8, 'A database was dropped'),
489498
'SERVER_STATUS_NO_BACKSLASH_ESCAPES': (1 << 9, ''),
499+
'SERVER_STATUS_METADATA_CHANGED': (1024,
500+
'Set if after a prepared statement '
501+
'reprepare we discovered that the '
502+
'new statement returns a different '
503+
'number of result set columns.'),
504+
'SERVER_QUERY_WAS_SLOW': (2048, ''),
505+
'SERVER_PS_OUT_PARAMS': (4096,
506+
'To mark ResultSet containing output '
507+
'parameter values.'),
508+
'SERVER_STATUS_IN_TRANS_READONLY': (8192,
509+
'Set if multi-statement '
510+
'transaction is a read-only '
511+
'transaction.'),
512+
'SERVER_SESSION_STATE_CHANGED': (1 << 14,
513+
'Session state has changed on the '
514+
'server because of the execution of '
515+
'the last statement'),
490516
}
491517

492518

lib/mysql/connector/cursor.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
from . import errors
3232
from .abstracts import MySQLCursorAbstract, NAMED_TUPLE_CACHE
3333
from .catch23 import PY2
34+
from .constants import ServerFlag
3435

3536
SQL_COMMENT = r"\/\*.*?\*\/"
3637
RE_SQL_COMMENT = re.compile(
@@ -60,6 +61,7 @@
6061

6162
ERR_NO_RESULT_TO_FETCH = "No result set to fetch from"
6263

64+
MAX_RESULTS = 4294967295
6365

6466
class _ParamSubstitutor(object):
6567
"""
@@ -1080,6 +1082,36 @@ def __init__(self, connection=None):
10801082
self._prepared = None
10811083
self._binary = True
10821084
self._have_result = None
1085+
self._last_row_sent = False
1086+
self._cursor_exists = False
1087+
1088+
def reset(self, free=True):
1089+
if self._prepared:
1090+
try:
1091+
self._connection.cmd_stmt_close(self._prepared['statement_id'])
1092+
except errors.Error:
1093+
# We tried to deallocate, but it's OK when we fail.
1094+
pass
1095+
self._prepared = None
1096+
self._last_row_sent = False
1097+
self._cursor_exists = False
1098+
1099+
def _handle_noresultset(self, res):
1100+
self._handle_server_status(res.get('status_flag',
1101+
res.get('server_status', 0)))
1102+
super(MySQLCursorPrepared, self)._handle_noresultset(res)
1103+
1104+
def _handle_server_status(self, flags):
1105+
"""Check for SERVER_STATUS_CURSOR_EXISTS and
1106+
SERVER_STATUS_LAST_ROW_SENT flags set by the server.
1107+
"""
1108+
self._cursor_exists = flags & ServerFlag.STATUS_CURSOR_EXISTS != 0
1109+
self._last_row_sent = flags & ServerFlag.STATUS_LAST_ROW_SENT != 0
1110+
1111+
def _handle_eof(self, eof):
1112+
self._handle_server_status(eof.get('status_flag',
1113+
eof.get('server_status', 0)))
1114+
super(MySQLCursorPrepared, self)._handle_eof(eof)
10831115

10841116
def callproc(self, *args, **kwargs):
10851117
"""Calls a stored procedue
@@ -1094,13 +1126,7 @@ def close(self):
10941126
This method will try to deallocate the prepared statement and close
10951127
the cursor.
10961128
"""
1097-
if self._prepared:
1098-
try:
1099-
self._connection.cmd_stmt_close(self._prepared['statement_id'])
1100-
except errors.Error:
1101-
# We tried to deallocate, but it's OK when we fail.
1102-
pass
1103-
self._prepared = None
1129+
self.reset()
11041130
super(MySQLCursorPrepared, self).close()
11051131

11061132
def _row_to_python(self, rowdata, desc=None):
@@ -1122,6 +1148,11 @@ def _handle_result(self, res):
11221148
self._connection.unread_result = True
11231149
self._have_result = True
11241150

1151+
if 'status_flag' in res[2]:
1152+
self._handle_server_status(res[2]['status_flag'])
1153+
elif 'server_status' in res[2]:
1154+
self._handle_server_status(res[2]['server_status'])
1155+
11251156
def execute(self, operation, params=(), multi=False): # multi is unused
11261157
"""Prepare and execute a MySQL Prepared Statement
11271158
@@ -1199,6 +1230,8 @@ def fetchone(self):
11991230
12001231
Returns a tuple or None.
12011232
"""
1233+
if self._cursor_exists:
1234+
self._connection.cmd_stmt_fetch(self._prepared['statement_id'])
12021235
return self._fetch_row() or None
12031236

12041237
def fetchmany(self, size=None):
@@ -1214,10 +1247,18 @@ def fetchmany(self, size=None):
12141247
def fetchall(self):
12151248
if not self._have_unread_result():
12161249
raise errors.InterfaceError("No result set to fetch from.")
1217-
(rows, eof) = self._connection.get_rows(
1218-
binary=self._binary, columns=self.description)
1250+
rows = []
1251+
if self._nextrow[0]:
1252+
rows.append(self._nextrow[0])
1253+
while self._have_unread_result():
1254+
if self._cursor_exists:
1255+
self._connection.cmd_stmt_fetch(
1256+
self._prepared['statement_id'], MAX_RESULTS)
1257+
(tmp, eof) = self._connection.get_rows(
1258+
binary=self._binary, columns=self.description)
1259+
rows.extend(tmp)
1260+
self._handle_eof(eof)
12191261
self._rowcount = len(rows)
1220-
self._handle_eof(eof)
12211262
return rows
12221263

12231264

lib/mysql/connector/protocol.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ def make_command(self, command, argument=None):
123123
data += argument
124124
return data
125125

126+
def make_stmt_fetch(self, statement_id, rows=1):
127+
"""Make a MySQL packet with Fetch Statement command"""
128+
return utils.int4store(statement_id) + utils.int4store(rows)
129+
126130
def make_change_user(self, handshake, username=None, password=None,
127131
database=None, charset=33, client_flags=0,
128132
ssl_enabled=False, auth_plugin=None):

0 commit comments

Comments
 (0)