Skip to content

Commit 939f268

Browse files
committed
BUG26834307: Make cursor.fetchone() and cursor.fetchmany() PEP 249 compliant
The PEP 249 specifies that an exception must be raised if the previous call to cursor.execute*() does not produce any result set or no call was issued yet. This patch fixes cursor.fetchone() and cursor.fetchmany() current behavior to comply with PEP 249. Tests were added for regression.
1 parent d9951ac commit 939f268

File tree

6 files changed

+147
-76
lines changed

6 files changed

+147
-76
lines changed

lib/mysql/connector/cursor.py

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,14 @@ def _have_unread_result(self):
367367
except AttributeError:
368368
return False
369369

370+
def _check_executed(self):
371+
"""Check if the statement has been executed.
372+
373+
Raises an error if the statement has not been executed.
374+
"""
375+
if self._executed is None:
376+
raise errors.InterfaceError(ERR_NO_RESULT_TO_FETCH)
377+
370378
def next(self):
371379
"""Used for iterating over the result set."""
372380
return self.__next__()
@@ -667,6 +675,7 @@ def executemany(self, operation, seq_params):
667675
return None
668676
stmt = self._batch_insert(operation, seq_params)
669677
if stmt is not None:
678+
self._executed = stmt
670679
return self.execute(stmt)
671680

672681
rowcnt = 0
@@ -875,12 +884,11 @@ def fetchone(self):
875884
876885
Returns a tuple or None.
877886
"""
878-
row = self._fetch_row()
879-
if row:
880-
return row
881-
return None
887+
self._check_executed()
888+
return self._fetch_row()
882889

883890
def fetchmany(self, size=None):
891+
self._check_executed()
884892
res = []
885893
cnt = (size or self.arraysize)
886894
while cnt > 0 and self._have_unread_result():
@@ -891,8 +899,10 @@ def fetchmany(self, size=None):
891899
return res
892900

893901
def fetchall(self):
902+
self._check_executed()
894903
if not self._have_unread_result():
895-
raise errors.InterfaceError("No result set to fetch from.")
904+
return []
905+
896906
(rows, eof) = self._connection.get_rows()
897907
if self._nextrow[0]:
898908
rows.insert(0, self._nextrow[0])
@@ -995,20 +1005,19 @@ def fetchone(self):
9951005
9961006
Returns a tuple or None.
9971007
"""
998-
row = self._fetch_row()
999-
if row:
1000-
return row
1001-
return None
1008+
self._check_executed()
1009+
return self._fetch_row()
10021010

10031011
def fetchall(self):
1004-
if self._rows is None:
1005-
raise errors.InterfaceError("No result set to fetch from.")
1012+
if self._executed is None or self._rows is None:
1013+
raise errors.InterfaceError(ERR_NO_RESULT_TO_FETCH)
10061014
res = []
10071015
res = self._rows[self._next_row:]
10081016
self._next_row = len(self._rows)
10091017
return res
10101018

10111019
def fetchmany(self, size=None):
1020+
self._check_executed()
10121021
res = []
10131022
cnt = (size or self.arraysize)
10141023
while cnt > 0:
@@ -1032,15 +1041,13 @@ class MySQLCursorRaw(MySQLCursor):
10321041
_raw = True
10331042

10341043
def fetchone(self):
1035-
row = self._fetch_row(raw=True)
1036-
1037-
if row:
1038-
return row
1039-
return None
1044+
self._check_executed()
1045+
return self._fetch_row(raw=True)
10401046

10411047
def fetchall(self):
1048+
self._check_executed()
10421049
if not self._have_unread_result():
1043-
raise errors.InterfaceError("No result set to fetch from.")
1050+
return []
10441051
(rows, eof) = self._connection.get_rows(raw=True)
10451052
if self._nextrow[0]:
10461053
rows.insert(0, self._nextrow[0])
@@ -1071,14 +1078,11 @@ def _handle_resultset(self):
10711078
pass
10721079

10731080
def fetchone(self):
1074-
row = self._fetch_row()
1075-
if row:
1076-
return row
1077-
return None
1081+
self._check_executed()
1082+
return self._fetch_row()
10781083

10791084
def fetchall(self):
1080-
if self._rows is None:
1081-
raise errors.InterfaceError("No result set to fetch from.")
1085+
self._check_executed()
10821086
return [r for r in self._rows[self._next_row:]]
10831087

10841088
@property
@@ -1247,11 +1251,13 @@ def fetchone(self):
12471251
12481252
Returns a tuple or None.
12491253
"""
1254+
self._check_executed()
12501255
if self._cursor_exists:
12511256
self._connection.cmd_stmt_fetch(self._prepared['statement_id'])
12521257
return self._fetch_row() or None
12531258

12541259
def fetchmany(self, size=None):
1260+
self._check_executed()
12551261
res = []
12561262
cnt = (size or self.arraysize)
12571263
while cnt > 0 and self._have_unread_result():
@@ -1262,8 +1268,7 @@ def fetchmany(self, size=None):
12621268
return res
12631269

12641270
def fetchall(self):
1265-
if not self._have_unread_result():
1266-
raise errors.InterfaceError("No result set to fetch from.")
1271+
self._check_executed()
12671272
rows = []
12681273
if self._nextrow[0]:
12691274
rows.append(self._nextrow[0])
@@ -1305,6 +1310,7 @@ def _row_to_python(self, rowdata, desc=None):
13051310
def fetchone(self):
13061311
"""Returns next row of a query result set
13071312
"""
1313+
self._check_executed()
13081314
row = self._fetch_row()
13091315
if row:
13101316
return self._row_to_python(row, self.description)
@@ -1313,8 +1319,10 @@ def fetchone(self):
13131319
def fetchall(self):
13141320
"""Returns all rows of a query result set
13151321
"""
1322+
self._check_executed()
13161323
if not self._have_unread_result():
1317-
raise errors.InterfaceError(ERR_NO_RESULT_TO_FETCH)
1324+
return []
1325+
13181326
(rows, eof) = self._connection.get_rows()
13191327
if self._nextrow[0]:
13201328
rows.insert(0, self._nextrow[0])
@@ -1359,6 +1367,7 @@ def _row_to_python(self, rowdata, desc=None):
13591367
def fetchone(self):
13601368
"""Returns next row of a query result set
13611369
"""
1370+
self._check_executed()
13621371
row = self._fetch_row()
13631372
if row:
13641373
if hasattr(self._connection, 'converter'):
@@ -1369,8 +1378,10 @@ def fetchone(self):
13691378
def fetchall(self):
13701379
"""Returns all rows of a query result set
13711380
"""
1381+
self._check_executed()
13721382
if not self._have_unread_result():
1373-
raise errors.InterfaceError(ERR_NO_RESULT_TO_FETCH)
1383+
return []
1384+
13741385
(rows, eof) = self._connection.get_rows()
13751386
if self._nextrow[0]:
13761387
rows.insert(0, self._nextrow[0])
@@ -1392,6 +1403,7 @@ class MySQLCursorBufferedDict(MySQLCursorDict, MySQLCursorBuffered):
13921403
def fetchone(self):
13931404
"""Returns next row of a query result set
13941405
"""
1406+
self._check_executed()
13951407
row = self._fetch_row()
13961408
if row:
13971409
return self._row_to_python(row, self.description)
@@ -1400,7 +1412,7 @@ def fetchone(self):
14001412
def fetchall(self):
14011413
"""Returns all rows of a query result set
14021414
"""
1403-
if self._rows is None:
1415+
if self._executed is None or self._rows is None:
14041416
raise errors.InterfaceError(ERR_NO_RESULT_TO_FETCH)
14051417
res = []
14061418
for row in self._rows[self._next_row:]:
@@ -1417,6 +1429,7 @@ class MySQLCursorBufferedNamedTuple(MySQLCursorNamedTuple, MySQLCursorBuffered):
14171429
def fetchone(self):
14181430
"""Returns next row of a query result set
14191431
"""
1432+
self._check_executed()
14201433
row = self._fetch_row()
14211434
if row:
14221435
return self._row_to_python(row, self.description)
@@ -1425,7 +1438,7 @@ def fetchone(self):
14251438
def fetchall(self):
14261439
"""Returns all rows of a query result set
14271440
"""
1428-
if self._rows is None:
1441+
if self._executed is None or self._rows is None:
14291442
raise errors.InterfaceError(ERR_NO_RESULT_TO_FETCH)
14301443
res = []
14311444
for row in self._rows[self._next_row:]:

lib/mysql/connector/cursor_cext.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
1+
# Copyright (c) 2014, 2020, Oracle and/or its affiliates. All rights reserved.
22
#
33
# This program is free software; you can redistribute it and/or modify
44
# it under the terms of the GNU General Public License, version 2.0, as
@@ -47,6 +47,8 @@
4747
RE_SQL_SPLIT_STMTS, RE_SQL_FIND_PARAM
4848
)
4949

50+
ERR_NO_RESULT_TO_FETCH = "No result set to fetch from"
51+
5052

5153
class _ParamSubstitutor(object):
5254

@@ -113,12 +115,20 @@ def reset(self, free=True):
113115
self._warnings = None
114116
self._warning_count = 0
115117
self._description = None
116-
self._executed = None
117118
self._executed_list = []
118119
if free and self._cnx:
119120
self._cnx.free_result()
120121
super(CMySQLCursor, self).reset()
121122

123+
124+
def _check_executed(self):
125+
"""Check if the statement has been executed.
126+
127+
Raises an error if the statement has not been executed.
128+
"""
129+
if self._executed is None:
130+
raise errors.InterfaceError(ERR_NO_RESULT_TO_FETCH)
131+
122132
def _fetch_warnings(self):
123133
"""Fetch warnings
124134
@@ -349,6 +359,7 @@ def executemany(self, operation, seq_params):
349359
return None
350360
stmt = self._batch_insert(operation, seq_params)
351361
if stmt is not None:
362+
self._executed = stmt
352363
return self.execute(stmt)
353364

354365
rowcnt = 0
@@ -485,8 +496,10 @@ def fetchall(self):
485496
486497
Returns a list of tuples.
487498
"""
499+
self._check_executed()
488500
if not self._cnx.unread_result:
489-
raise errors.InterfaceError("No result set to fetch from.")
501+
return []
502+
490503
rows = self._cnx.get_rows()
491504
if self._nextrow and self._nextrow[0]:
492505
rows[0].insert(0, self._nextrow[0])
@@ -502,6 +515,7 @@ def fetchall(self):
502515

503516
def fetchmany(self, size=1):
504517
"""Returns the next set of rows of a result set"""
518+
self._check_executed()
505519
if self._nextrow and self._nextrow[0]:
506520
rows = [self._nextrow[0]]
507521
size -= 1
@@ -529,6 +543,7 @@ def fetchmany(self, size=1):
529543

530544
def fetchone(self):
531545
"""Returns next row of a query result set"""
546+
self._check_executed()
532547
row = self._nextrow
533548
if not row and self._cnx.unread_result:
534549
row = self._cnx.get_row()
@@ -678,13 +693,13 @@ def _fetch_row(self):
678693
return row
679694

680695
def fetchall(self):
681-
if self._rows is None:
682-
raise errors.InterfaceError("No result set to fetch from.")
696+
self._check_executed()
683697
res = self._rows[self._next_row:]
684698
self._next_row = len(self._rows)
685699
return res
686700

687701
def fetchmany(self, size=1):
702+
self._check_executed()
688703
res = []
689704
cnt = size or self.arraysize
690705
while cnt > 0:
@@ -697,6 +712,7 @@ def fetchmany(self, size=1):
697712
return res
698713

699714
def fetchone(self):
715+
self._check_executed()
700716
return self._fetch_row()
701717

702718

@@ -783,6 +799,8 @@ def fetchone(self):
783799
def fetchmany(self, size=1):
784800
"""Returns next set of rows as list of named tuples"""
785801
res = super(CMySQLCursorNamedTuple, self).fetchmany(size=size)
802+
if not res:
803+
return []
786804
return [self.named_tuple(*res[0])]
787805

788806
def fetchall(self):
@@ -974,13 +992,15 @@ def fetchone(self):
974992
975993
Returns a tuple or None.
976994
"""
995+
self._check_executed()
977996
return self._fetch_row() or None
978997

979998
def fetchmany(self, size=None):
980999
"""Returns the next set of rows of a result set
9811000
9821001
Returns a list of tuples.
9831002
"""
1003+
self._check_executed()
9841004
res = []
9851005
cnt = size or self.arraysize
9861006
while cnt > 0 and self._stmt.have_result_set:
@@ -995,8 +1015,10 @@ def fetchall(self):
9951015
9961016
Returns a list of tuples.
9971017
"""
1018+
self._check_executed()
9981019
if not self._stmt.have_result_set:
999-
raise errors.InterfaceError("No result set to fetch from.")
1020+
return []
1021+
10001022
rows = self._cnx.get_rows(prep_stmt=self._stmt)
10011023
if self._nextrow and self._nextrow[0]:
10021024
rows[0].insert(0, self._nextrow[0])

tests/cext/test_cext_cursor.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# -*- coding: utf-8 -*-
22

3-
# Copyright (c) 2014, 2019, Oracle and/or its affiliates. All rights reserved.
3+
# Copyright (c) 2014, 2020, Oracle and/or its affiliates. All rights reserved.
44
#
55
# This program is free software; you can redistribute it and/or modify
66
# it under the terms of the GNU General Public License, version 2.0, as
@@ -178,7 +178,6 @@ def test_executemany__errors(self):
178178
"INSERT INTO t1 1 %s", [(1,), (2,)])
179179

180180
cur.executemany("SELECT SHA1(%s)", [('foo',), ('bar',)])
181-
self.assertEqual(None, cur.fetchone())
182181

183182
def test_executemany(self):
184183
tbl = 'myconnpy_cursor'
@@ -346,7 +345,7 @@ def test_callproc(self):
346345
def test_fetchone(self):
347346
cur = self._get_cursor(self.cnx)
348347

349-
self.assertEqual(None, cur.fetchone())
348+
self.assertRaises(errors.InterfaceError, cur.fetchone)
350349

351350
cur = self.cnx.cursor()
352351
cur.execute("SELECT BINARY 'ham'")
@@ -359,7 +358,7 @@ def test_fetchmany(self):
359358
"""MySQLCursor object fetchmany()-method"""
360359
cur = self._get_cursor(self.cnx)
361360

362-
self.assertEqual([], cur.fetchmany())
361+
self.assertRaises(errors.InterfaceError, cur.fetchmany)
363362

364363
tbl = 'myconnpy_fetch'
365364
self.setup_table(self.cnx, tbl)
@@ -620,7 +619,7 @@ def _get_cursor(self, cnx=None):
620619

621620
def test_fetchone(self):
622621
cur = self._get_cursor(self.cnx)
623-
self.assertEqual(None, cur.fetchone())
622+
self.assertRaises(errors.InterfaceError, cur.fetchone)
624623

625624
cur.execute("SELECT 1, 'string', MAKEDATE(2010,365), 2.5")
626625
exp = (b'1', b'string', b'2010-12-31', b'2.5')

0 commit comments

Comments
 (0)