From 5bf3dc7656fd68b9b9778f0d1f5da39b65d15782 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:27:27 +0000 Subject: [PATCH 1/4] Initial plan From b01694a3cf155d4ef6d7f6a4ca391f72443ff0ac Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Jun 2026 07:29:23 +0000 Subject: [PATCH 2/4] Quote procname and user variables with backticks in callproc --- src/MySQLdb/cursors.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/MySQLdb/cursors.py b/src/MySQLdb/cursors.py index 2f781f70..20274a0e 100644 --- a/src/MySQLdb/cursors.py +++ b/src/MySQLdb/cursors.py @@ -23,6 +23,10 @@ ) +def _backquote_escape(s): + return s.replace(b"`", b"``") + + class BaseCursor: """A base for Cursor classes. Useful attributes: @@ -279,10 +283,10 @@ def callproc(self, procname, args=()): variable and then retrieved by a query. Since stored procedures return zero or more result sets, there is no reliable way to get at OUT or INOUT parameters via callproc. - The server variables are named @_procname_n, where procname + The server variables are named @`_procname_n`, where procname is the parameter above and n is the position of the parameter (from zero). Once all result sets generated by the procedure - have been fetched, you can issue a SELECT @_procname_0, ... + have been fetched, you can issue a SELECT @`_procname_0`, ... query using .execute() to get any OUT or INOUT values. Compatibility warning: The act of calling a stored procedure @@ -295,17 +299,18 @@ def callproc(self, procname, args=()): db = self._get_db() if isinstance(procname, str): procname = procname.encode(db.encoding) + procname_escaped = _backquote_escape(procname) if args: - fmt = b"@_" + procname + b"_%d=%s" + fmt = b"@`_" + procname_escaped + b"_%d`=%s" q = b"SET %s" % b",".join( fmt % (index, db.literal(arg)) for index, arg in enumerate(args) ) self._query(q) self.nextset() - q = b"CALL %s(%s)" % ( - procname, - b",".join([b"@_%s_%d" % (procname, i) for i in range(len(args))]), + q = b"CALL `%s`(%s)" % ( + procname_escaped, + b",".join([b"@`_%s_%d`" % (procname_escaped, i) for i in range(len(args))]), ) self._query(q) return args From a3171eee8e7dde19c3b3c1c0372c575abf7e6bfb Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 11 Jun 2026 17:23:43 +0900 Subject: [PATCH 3/4] add test --- tests/test_cursor.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_cursor.py b/tests/test_cursor.py index 65d9c04e..d40752f5 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -2,6 +2,7 @@ import MySQLdb.cursors from MySQLdb.constants import ER from configdb import connection_factory +from textwrap import dedent _conns = [] @@ -308,3 +309,24 @@ def test_cursor_is_iterator(Cursor): assert next(cursor) == ("c",) with pytest.raises(StopIteration): next(cursor) + + +def test_callproc_escaping(): + conn = connect() + cur = conn.cursor() + + cur.execute("DROP PROCEDURE IF EXISTS `foo.bar`") + try: + cur.execute( + dedent("""\ + create procedure `foo.bar` (arg1 int) + begin + select arg1*2; + end + """) + ) + + cur.callproc("foo.bar", args=(123,)) + self.assertEqual(cur.fetchone()[0], 246) + finally: + cur.execute("DROP PROCEDURE IF EXISTS `foo.bar`") From 0d33ff965d8ed585cb7aec4a880f847ce5a6f8dc Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Thu, 11 Jun 2026 23:55:37 +0900 Subject: [PATCH 4/4] fix test --- tests/test_cursor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cursor.py b/tests/test_cursor.py index d40752f5..877e5925 100644 --- a/tests/test_cursor.py +++ b/tests/test_cursor.py @@ -327,6 +327,6 @@ def test_callproc_escaping(): ) cur.callproc("foo.bar", args=(123,)) - self.assertEqual(cur.fetchone()[0], 246) + assert cur.fetchone()[0] == 246 finally: cur.execute("DROP PROCEDURE IF EXISTS `foo.bar`")