Skip to content
Draft
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
41 changes: 41 additions & 0 deletions doc/user_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -536,6 +536,47 @@ rollback()
(cancels) the current transaction; otherwise a
``NotSupportedError`` is raised.

reset()
Reset the session using ``mysql_reset_connection()``
(``COM_RESET_CONNECTION``), returning ``None`` on success. This rolls
back active transactions, drops temporary tables, releases locks and
prepared statements, and clears user variables. Session system variables,
including autocommit and SQL mode, follow the server's reset behavior.
Autocommit is **not** restored to the value passed to ``connect()``.
``get_autocommit()`` reads the status returned by the server.

MySQL 8.4 has a known limitation when global autocommit is OFF: reset
restores the session variable to OFF but reports ON in protocol status.
This method does not normalize that inconsistent server state. With the
usual global autocommit ON, call ``autocommit(False)`` after resetting if
needed. MariaDB's reset status agrees with its global autocommit value.

The authenticated user, current database and connection ID are unchanged.
The current client character set and any collation explicitly configured
through ``connect()`` or ``set_character_set()`` are reapplied so that
server settings, C-library escaping and Python encoding agree. Calling
``set_character_set()`` without a collation selects the charset's default
collation and clears any previously configured collation. Changes made
through arbitrary SQL are not tracked or preserved. ``init_command`` and
other connection initialization settings are not replayed. ``messages``
is cleared; Python conversion and cursor configuration are retained.

Consume every result set and close existing cursors before resetting;
create new cursors afterwards. Unread results raise ``ProgrammingError``.
Closed or lost connections raise the usual connection exceptions. Reset
never reconnects, even after ``ping(True)``, and does not fall back to
``change_user()`` or another reset command. If reset succeeds but restoring
the charset or collation fails, the connection is closed and the error
is raised.

Requires a server supporting ``COM_RESET_CONNECTION`` and a build using
MySQL client library 5.7.3+ or MariaDB Connector/C 3.0.0+. Older client
libraries raise ``NotSupportedError``; server errors are propagated.
See the `MySQL reset API documentation
<https://dev.mysql.com/doc/c-api/8.4/en/mysql-reset-connection.html>`_
and `MariaDB Connector/C reset API documentation
<https://mariadb.com/docs/connectors/mariadb-connector-c/api-functions/mysql_reset_connection>`_.

cursor([cursorclass])
MySQL does not support cursors; however, cursors are easily
emulated. You can supply an alternative cursor class as an
Expand Down
128 changes: 128 additions & 0 deletions src/MySQLdb/_mysql.c
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ PERFORMANCE OF THIS SOFTWARE.
#define HAVE_MYSQL_OPT_LOCAL_INFILE_DIR
#endif

/* MariaDB's MYSQL_VERSION_ID describes the server, not Connector/C. */
#if defined(MARIADB_PACKAGE_VERSION_ID)
#if MARIADB_PACKAGE_VERSION_ID >= 30000
#define HAVE_MYSQL_RESET_CONNECTION
#endif
#elif !defined(MARIADB_BASE_VERSION) && !defined(MARIADB_VERSION_ID) && MYSQL_VERSION_ID >= 50703
#define HAVE_MYSQL_RESET_CONNECTION
#endif

#define PY_SSIZE_T_CLEAN 1
#include "Python.h"

Expand Down Expand Up @@ -969,6 +978,113 @@ _mysql_ConnectionObject_rollback(
Py_RETURN_NONE;
}

static const char _mysql_ConnectionObject_reset__doc__[] =
"Reset session state without reconnecting. Returns None.\n\
Rolls back transactions and restores server session defaults, including\n\
autocommit. Preserves the client character set with its default collation.\n\
Consume pending results before calling. Requires mysql_reset_connection().\n\
";

static PyObject *
_mysql_ConnectionObject_Reset(
_mysql_ConnectionObject *self,
const char *collation)
{
BEGIN_CONNECTION_OPERATION(self, return _mysql_Exception(self));
#ifdef HAVE_MYSQL_RESET_CONNECTION
/* Connector/C can discard unread results, unlike libmysqlclient. Do not
* invalidate a live streaming result or silently discard result sets. */
if (self->connection.status != MYSQL_STATUS_READY ||
mysql_more_results(&self->connection)) {
END_CONNECTION_LOCK(self);
PyErr_SetString(_mysql_ProgrammingError,
"Consume all pending results before resetting the connection.");
return NULL;
}

const char *charset = mysql_character_set_name(&self->connection);
PyObject *collation_query = NULL;
const char *collation_sql = NULL;
unsigned long collation_sql_len = 0;
if (collation && *collation) {
collation_query = PyBytes_FromFormat(
"SET NAMES %s COLLATE %s", charset, collation);
if (!collation_query) {
END_CONNECTION_LOCK(self);
return NULL;
}
collation_sql = PyBytes_AS_STRING(collation_query);
collation_sql_len = (unsigned long)PyBytes_GET_SIZE(collation_query);
}
int err, restore_error = 0;
my_bool reconnect = (my_bool)self->reconnect;
#ifdef MARIADB_PACKAGE_VERSION_ID
/* Connector/C option files can enable reconnect without ping(True). */
mysql_get_option(&self->connection, MYSQL_OPT_RECONNECT, &reconnect);
#endif
/* ping(True) enables persistent automatic reconnection. Suppress it for
* both reset and charset restoration, without changing that preference. */
if (reconnect) {
my_bool disabled = 0;
mysql_options(&self->connection, MYSQL_OPT_RECONNECT, &disabled);
}
Py_BEGIN_ALLOW_THREADS
err = mysql_reset_connection(&self->connection);
if (!err) {
/* Reset changes server charset variables but leaves the C library's
* charset unchanged. Keep escaping and Python encoding coherent. */
err = restore_error = mysql_set_character_set(&self->connection, charset);
}
if (!err && collation_sql) {
err = restore_error = mysql_real_query(
&self->connection, collation_sql, collation_sql_len);
}
Py_END_ALLOW_THREADS
Py_XDECREF(collation_query);
PyObject *ret = err ? _mysql_Exception(self) : NULL;
if (restore_error) {
/* The reset succeeded but charset/collation restoration did not. This
* connection must not be reused with mismatched client/server state. */
Py_BEGIN_ALLOW_THREADS
mysql_close(&self->connection);
Py_END_ALLOW_THREADS
self->open = false;
_mysql_ConnectionObject_clear(self);
}
else if (reconnect) {
mysql_options(&self->connection, MYSQL_OPT_RECONNECT, &reconnect);
}
END_CONNECTION_LOCK(self);
if (err) return ret;
Py_RETURN_NONE;
#else
END_CONNECTION_LOCK(self);
PyErr_SetString(_mysql_NotSupportedError,
"reset() requires MySQL client library >= 5.7.3 or MariaDB Connector/C >= 3.0.0");
return NULL;
#endif
}

static PyObject *
_mysql_ConnectionObject_reset(
_mysql_ConnectionObject *self,
PyObject *noargs)
{
return _mysql_ConnectionObject_Reset(self, NULL);
}

/* Used by Connection.reset() to restore its explicitly configured collation
* within the same lock and reconnect suppression as the native reset. */
static PyObject *
_mysql_ConnectionObject_reset_with_collation(
_mysql_ConnectionObject *self,
PyObject *args)
{
const char *collation;
if (!PyArg_ParseTuple(args, "z:_reset_with_collation", &collation)) return NULL;
return _mysql_ConnectionObject_Reset(self, collation);
}

static const char _mysql_ConnectionObject_next_result__doc__[] =
"If more query results exist, next_result() reads the next query\n\
results and returns the status back to application.\n\
Expand Down Expand Up @@ -2622,6 +2738,18 @@ static PyMethodDef _mysql_ConnectionObject_methods[] = {
METH_VARARGS,
_mysql_ConnectionObject_autocommit__doc__
},
{
"reset",
(PyCFunction)_mysql_ConnectionObject_reset,
METH_NOARGS,
_mysql_ConnectionObject_reset__doc__
},
{
"_reset_with_collation",
(PyCFunction)_mysql_ConnectionObject_reset_with_collation,
METH_VARARGS,
NULL
},
{
"get_autocommit",
(PyCFunction)_mysql_ConnectionObject_get_autocommit,
Expand Down
19 changes: 16 additions & 3 deletions src/MySQLdb/connections.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,7 @@ class object, used to create cursors (keyword only)
"executemany_fallback", self.executemany_fallback
)
if executemany_fallback not in ("loop", "multi"):
raise ValueError(
"executemany_fallback must be either 'loop' or 'multi'"
)
raise ValueError("executemany_fallback must be either 'loop' or 'multi'")

client_flag = kwargs.get("client_flag", 0)
client_flag |= CLIENT.MULTI_RESULTS
Expand Down Expand Up @@ -295,6 +293,17 @@ def cursor(self, cursorclass=None):
"""
return (cursorclass or self.cursorclass)(self)

def reset(self):
"""Reset the session without reconnecting; return None.

Roll back transactions and restore server session defaults, including
autocommit. Keep the current client charset and explicitly set collation.
Connection initialization commands are not replayed. Consume all results
and close cursors before calling; create new cursors after the reset.
"""
super()._reset_with_collation(self._collation)
self.messages.clear()

def query(self, query):
# Since _mysql releases GIL while querying, we need immutable buffer.
if isinstance(query, bytearray):
Expand Down Expand Up @@ -343,9 +352,13 @@ def set_character_set(self, charset, collation=None):
"""Set the connection character set to charset."""
super().set_character_set(charset)
self.encoding = _charset_to_encoding.get(charset, charset)
# The native call selected the default collation, even if the optional
# collation change below fails.
self._collation = None
if collation:
self.query(f"SET NAMES {charset} COLLATE {collation}")
self.store_result()
self._collation = collation

def set_sql_mode(self, sql_mode):
"""Set the connection sql_mode. See MySQL documentation for
Expand Down
11 changes: 5 additions & 6 deletions tests/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
import pytest
from configdb import connection_factory, connection_kwargs

from MySQLdb.connections import Connection
from MySQLdb._exceptions import ProgrammingError
from MySQLdb.connections import Connection
from MySQLdb.constants import CLIENT


Expand Down Expand Up @@ -63,9 +63,7 @@ class MultiConnection(Connection):
with MultiConnection(**connection_kwargs({})) as conn:
assert conn.executemany_fallback == "multi"

with MultiConnection(
**connection_kwargs({"executemany_fallback": "loop"})
) as conn:
with MultiConnection(**connection_kwargs({"executemany_fallback": "loop"})) as conn:
assert conn.executemany_fallback == "loop"


Expand All @@ -74,7 +72,8 @@ def test_ping_false_warns(conn):
conn.ping(False)


def test_connection_concurrent_use_raises(conn):
@pytest.mark.parametrize("method", ["thread_id", "reset"])
def test_connection_concurrent_use_raises(conn, method):
"""While a slow query holds the connection lock, any other access from
a second thread must raise ProgrammingError immediately (not block)."""
thread_error = None
Expand All @@ -99,7 +98,7 @@ def run_slow_query():

start = time.monotonic()
with pytest.raises(ProgrammingError, match="already in use"):
conn.thread_id()
getattr(conn, method)()
# Should fail immediately, not wait for the SLEEP to finish.
assert time.monotonic() - start < 0.1

Expand Down
Loading
Loading