From d512d6a6b2da86ea0c558b4b151ca38970b876aa Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 17 May 2023 00:58:08 +0900 Subject: [PATCH 1/6] Add test for discard partial result --- tests/test_cursor.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/test_cursor.py b/tests/test_cursor.py index c681b63b..5cb98910 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -1,4 +1,4 @@ -# import pytest +import pytest import MySQLdb.cursors from configdb import connection_factory @@ -186,3 +186,39 @@ def test_mogrify_with_dict_args(): assert mogrified_query == "SELECT 1, 2" assert mogrified_query == cursor._executed.decode() + + +# Test that cursor can be used without reading whole resultset. +@pytest.mark.parametrize("Cursor", [MySQLdb.cursors.Cursor, MySQLdb.cursors.SSCursor]) +def test_cursor_discard_result(Cursor): + conn = connect() + cursor = conn.cursor(Cursor) + + cursor.execute( + """\ +CREATE TABLE test_cursor_discard_result ( + id INTEGER PRIMARY KEY AUTO_INCREMENT, + data VARCHAR(100) +)""" + ) + _tables.append("test_cursor_discard_result") + + cursor.executemany( + "INSERT INTO test_cursor_discard_result (id, data) VALUES (%s, %s)", + [(i, f"row {i}") for i in range(1, 101)], + ) + + cursor.execute( + """\ +SELECT * FROM test_cursor_discard_result WHERE id <= 10; +SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 11 AND 20; +SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 21 AND 30; +""" + ) + cursor.nextset() + assert cursor.fetchone() == (11, "row 11") + + cursor.execute( + "SELECT * FROM test_cursor_discard_result WHERE id BETWEEN 31 AND 40" + ) + assert cursor.fetchone() == (31, "row 31") From 60bb6ee5f0dc88e541fa80f1eafe1a05ca64f6aa Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 17 May 2023 01:15:32 +0900 Subject: [PATCH 2/6] Add Connection.discard_result() --- src/MySQLdb/_mysql.c | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/MySQLdb/_mysql.c b/src/MySQLdb/_mysql.c index 4463f627..73fceef3 100644 --- a/src/MySQLdb/_mysql.c +++ b/src/MySQLdb/_mysql.c @@ -2081,6 +2081,36 @@ _mysql_ConnectionObject_use_result( return result; } +static const char _mysql_ConnectionObject_discard_result__doc__[] = +"Discard current result set.\n\n" +"This function can be called instead of use_result() or store_result(). Non-standard."; + +static PyObject * +_mysql_ConnectionObject_discard_result( + _mysql_ConnectionObject *self, + PyObject *noargs) +{ + check_connection(self); + MYSQL *conn = &(self->connection); + + Py_BEGIN_ALLOW_THREADS; + MYSQL_RES *res = mysql_use_result(conn); + if (res == NULL) { + Py_BLOCK_THREADS; + return _mysql_Exception(self); + } + MYSQL_ROW row; + while (NULL != (row = mysql_fetch_row(res))) { + // do nothing. + } + mysql_free_result(res); + Py_END_ALLOW_THREADS; + if (mysql_errno(conn)) { + return _mysql_Exception(self); + } + Py_RETURN_NONE; +} + static void _mysql_ConnectionObject_dealloc( _mysql_ConnectionObject *self) @@ -2376,6 +2406,12 @@ static PyMethodDef _mysql_ConnectionObject_methods[] = { METH_NOARGS, _mysql_ConnectionObject_use_result__doc__ }, + { + "discard_result", + (PyCFunction)_mysql_ConnectionObject_discard_result, + METH_NOARGS, + _mysql_ConnectionObject_discard_result__doc__ + }, {NULL, NULL} /* sentinel */ }; From 76c3be09244622bd9915fa755dbd9a4af2541433 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 17 May 2023 01:24:58 +0900 Subject: [PATCH 3/6] Add Result.discard() --- src/MySQLdb/_mysql.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/MySQLdb/_mysql.c b/src/MySQLdb/_mysql.c index 73fceef3..8bef86ee 100644 --- a/src/MySQLdb/_mysql.c +++ b/src/MySQLdb/_mysql.c @@ -1484,6 +1484,26 @@ _mysql_ResultObject_fetch_row( return NULL; } +static const char _mysql_ResultObject_discard__doc__[] = +"discard() -- Discard remaining rows in the resultset."; + +static PyObject * +_mysql_ResultObject_discard( + _mysql_ResultObject *self, + PyObject *noargs) +{ + check_result_connection(self); + + MYSQL_ROW row; + while (NULL != (row = mysql_fetch_row(self->result))) { + // do nothing + } + if (mysql_errno(self->conn)) { + return _mysql_Exception(self->conn); + } + Py_RETURN_NONE; +} + static char _mysql_ConnectionObject_change_user__doc__[] = "Changes the user and causes the database specified by db to\n\ become the default (current) database on the connection\n\ @@ -2473,6 +2493,12 @@ static PyMethodDef _mysql_ResultObject_methods[] = { METH_VARARGS | METH_KEYWORDS, _mysql_ResultObject_fetch_row__doc__ }, + { + "discard", + (PyCFunction)_mysql_ResultObject_discard, + METH_NOARGS, + _mysql_ResultObject_discard__doc__ + }, { "field_flags", (PyCFunction)_mysql_ResultObject_field_flags, From 36ba55e471a5b2b403bad8cd3cd43f5480ba232a Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 17 May 2023 01:29:58 +0900 Subject: [PATCH 4/6] Cursor doesn't create Python objects when discarding. --- src/MySQLdb/cursors.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/MySQLdb/cursors.py b/src/MySQLdb/cursors.py index fdf52c0b..cdd3ab8d 100644 --- a/src/MySQLdb/cursors.py +++ b/src/MySQLdb/cursors.py @@ -75,13 +75,23 @@ def __init__(self, connection): self.rownumber = None self._rows = None + def _discard(self): + if self._result: + self._result.discard() + self._result = None + + con = self.connection + if con is None: + return + while con.next_result(): + con.discard_result() + def close(self): """Close the cursor. No further queries will be possible.""" try: if self.connection is None: return - while self.nextset(): - pass + self._discard() finally: self.connection = None self._result = None @@ -180,8 +190,7 @@ def execute(self, query, args=None): Returns integer represents rows affected, if any """ - while self.nextset(): - pass + self._discard() mogrified_query = self._mogrify(query, args) From 5533274f925953656e1aa0c356467b109a1a406d Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 17 May 2023 01:34:45 +0900 Subject: [PATCH 5/6] Cleare more data --- src/MySQLdb/cursors.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/MySQLdb/cursors.py b/src/MySQLdb/cursors.py index cdd3ab8d..68783e8f 100644 --- a/src/MySQLdb/cursors.py +++ b/src/MySQLdb/cursors.py @@ -76,6 +76,13 @@ def __init__(self, connection): self._rows = None def _discard(self): + self.description = None + self.description_flags = None + self.rowcount = -1 + self.lastrowid = None + self._rows = None + self.rownumber = None + if self._result: self._result.discard() self._result = None From 38e444251a58ccf4d685f14c7f6c5742c653028e Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Wed, 17 May 2023 01:48:33 +0900 Subject: [PATCH 6/6] Fix bugs --- src/MySQLdb/_mysql.c | 9 ++++++++- src/MySQLdb/cursors.py | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/MySQLdb/_mysql.c b/src/MySQLdb/_mysql.c index 8bef86ee..b8a19d26 100644 --- a/src/MySQLdb/_mysql.c +++ b/src/MySQLdb/_mysql.c @@ -2114,11 +2114,17 @@ _mysql_ConnectionObject_discard_result( MYSQL *conn = &(self->connection); Py_BEGIN_ALLOW_THREADS; + MYSQL_RES *res = mysql_use_result(conn); if (res == NULL) { Py_BLOCK_THREADS; - return _mysql_Exception(self); + if (mysql_errno(conn) != 0) { + // fprintf(stderr, "mysql_use_result failed: %s\n", mysql_error(conn)); + return _mysql_Exception(self); + } + Py_RETURN_NONE; } + MYSQL_ROW row; while (NULL != (row = mysql_fetch_row(res))) { // do nothing. @@ -2126,6 +2132,7 @@ _mysql_ConnectionObject_discard_result( mysql_free_result(res); Py_END_ALLOW_THREADS; if (mysql_errno(conn)) { + // fprintf(stderr, "mysql_free_result failed: %s\n", mysql_error(conn)); return _mysql_Exception(self); } Py_RETURN_NONE; diff --git a/src/MySQLdb/cursors.py b/src/MySQLdb/cursors.py index 68783e8f..d3b2947b 100644 --- a/src/MySQLdb/cursors.py +++ b/src/MySQLdb/cursors.py @@ -90,7 +90,7 @@ def _discard(self): con = self.connection if con is None: return - while con.next_result(): + while con.next_result() == 0: # -1 means no more data. con.discard_result() def close(self):