Skip to content

Commit a55960b

Browse files
committed
BUG#21390859: STATEMENTS GET OUT OF SYNCH WITH RESULT SETS
When including callproc (calling stored procedures) statements as part of a multi-query string while executing `cursor.execute(multi_stmt_query, multi=True)` the statements get out of sync with the result, that's to say, the statement-result mapping isn't correct, especially when a procedure produces multiple results. This issue is fixed as part of BUG#35710145, however, no relevant tests for this use case were added during the fix of BUG#35710145. With this patch, we extend the unit tests to cover the scenario where multi-result `callproc` statements are included in a multi-statement query. Change-Id: I30917e9ced90e5d0978929a714b295115c7083ab
1 parent fdb0127 commit a55960b

File tree

2 files changed

+299
-0
lines changed

2 files changed

+299
-0
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ v8.3.0
1919
- BUG#35912790: Binary strings are converted when using prepared statements
2020
- BUG#35832148: Fix Django timezone.utc deprecation warning
2121
- BUG#35710145: Bad MySQLCursor.statement and result when query text contains code comments
22+
- BUG#21390859: STATEMENTS GET OUT OF SYNCH WITH RESULT SETS
2223

2324
v8.2.0
2425
======

tests/test_bugs.py

Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7848,3 +7848,301 @@ async def test_comments_with_execute_multi(self):
78487848
i += 1
78497849
except IndexError:
78507850
self.fail(f"Got more results than expected for query {op}")
7851+
7852+
7853+
class BugOra21390859(tests.MySQLConnectorTests):
7854+
"""BUG#21390859: STATEMENTS GET OUT OF SYNCH WITH RESULT SETS
7855+
7856+
When including callproc (calling stored procedures) statements as part of a
7857+
multi-query string while executing `cursor.execute(multi_stmt_query, multi=True)`
7858+
the statements get out of sync with the result, that's to say, the statement-result
7859+
mapping isn't correct, especially when a procedure produces multiple results.
7860+
7861+
This issue is fixed as part of BUG#35710145, however, no relevant tests for this
7862+
use case were added during the fix of BUG#35710145.
7863+
7864+
With this patch, we extend the unit tests to cover the scenario where multi-result
7865+
`callproc` statements are included in a multi-statement query.
7866+
7867+
Acronyms
7868+
rop: read operation
7869+
wop: write operation
7870+
1: it means the gotten result set isn't empty
7871+
0: it means the gotten result set is empty
7872+
"""
7873+
7874+
table_name = "BugOra21390859"
7875+
7876+
use_case, use_case_exp = {}, {}
7877+
7878+
use_case[
7879+
1
7880+
] = f"""
7881+
-- rop wrop proc_milti_read proc_multi_insert;
7882+
-- 1 0 1 1 1 0 0 ;
7883+
SELECT 76;
7884+
INSERT INTO {table_name} (city, country_id) VALUES ('Ciudad de Mexico', '38');
7885+
call {table_name}_multi_read(2);
7886+
call {table_name}_multi_insert();
7887+
"""
7888+
use_case_exp[1] = [
7889+
("SELECT 76", [(76,)]),
7890+
(
7891+
f"INSERT INTO {table_name} (city, country_id) VALUES ('Ciudad de Mexico', '38')",
7892+
[],
7893+
),
7894+
(f"call {table_name}_multi_read(2)", [(2,)]),
7895+
(f"call {table_name}_multi_read(2)", [(3,)]),
7896+
(f"call {table_name}_multi_read(2)", [("bar",)]),
7897+
(f"call {table_name}_multi_read(2)", []),
7898+
(f"call {table_name}_multi_insert()", []),
7899+
]
7900+
7901+
use_case[
7902+
2
7903+
] = f"""
7904+
SELECT 'foo';
7905+
INSERT INTO {table_name} (city, country_id) VALUES ('Ciudad de Mexico', '38');
7906+
call {table_name}_multi_read(4);
7907+
# rop wrop proc_milti_read;
7908+
-- 1 0 1 1 1 0 ;
7909+
"""
7910+
use_case_exp[2] = [
7911+
("SELECT 'foo'", [("foo",)]),
7912+
(
7913+
f"INSERT INTO {table_name} (city, country_id) VALUES ('Ciudad de Mexico', '38')",
7914+
[],
7915+
),
7916+
(f"call {table_name}_multi_read(4)", [(4,)]),
7917+
(f"call {table_name}_multi_read(4)", [(5,)]),
7918+
(f"call {table_name}_multi_read(4)", [("bar",)]),
7919+
(f"call {table_name}_multi_read(4)", []),
7920+
]
7921+
7922+
use_case[3] = use_case[1] + use_case[2]
7923+
use_case_exp[3] = use_case_exp[1] + use_case_exp[2]
7924+
7925+
use_case[4] = use_case[2] + use_case[1]
7926+
use_case_exp[4] = use_case_exp[2] + use_case_exp[1]
7927+
7928+
use_case[
7929+
5
7930+
] = f"""
7931+
-- one;
7932+
call {table_name}_single_read(1);
7933+
-- two;
7934+
call {table_name}_single_read(2);
7935+
-- three;
7936+
call {table_name}_single_read(3);
7937+
"""
7938+
use_case_exp[5] = [
7939+
(f"call {table_name}_single_read(1)", [(1,)]),
7940+
(f"call {table_name}_single_read(1)", []),
7941+
(f"call {table_name}_single_read(2)", [(2,)]),
7942+
(f"call {table_name}_single_read(2)", []),
7943+
(f"call {table_name}_single_read(3)", [(3,)]),
7944+
(f"call {table_name}_single_read(3)", []),
7945+
]
7946+
7947+
# same as 6, but ending the query with a EOL comment
7948+
use_case[6] = use_case[5] + "\n -- end;"
7949+
use_case_exp[6] = use_case_exp[5][:]
7950+
7951+
use_case[
7952+
7
7953+
] = f"""
7954+
SELECT 'Hello MySQL';
7955+
INSERT INTO {table_name} (city, country_id) VALUES ('Colima', '84');
7956+
SELECT 10; -- ten;
7957+
# testing
7958+
-- comment;
7959+
INSERT INTO {table_name} (city, country_id) VALUES ('Tabasco', '0'); # added tabasco;
7960+
-- comment;
7961+
-- comment;
7962+
-- comment;
7963+
-- two;
7964+
call {table_name}_read_and_insert();
7965+
-- three;
7966+
call {table_name}_multi_insert();
7967+
-- two;
7968+
"""
7969+
use_case_exp[7] = [
7970+
(f"SELECT 'Hello MySQL'", [("Hello MySQL",)]),
7971+
(
7972+
f"INSERT INTO {table_name} (city, country_id) VALUES ('Colima', '84')",
7973+
[],
7974+
),
7975+
(f"SELECT 10", [(10,)]),
7976+
(
7977+
f"INSERT INTO {table_name} (city, country_id) VALUES ('Tabasco', '0')",
7978+
[],
7979+
),
7980+
(f"call {table_name}_read_and_insert()", [("Oracle",)]),
7981+
(f"call {table_name}_read_and_insert()", [("MySQL",)]),
7982+
(f"call {table_name}_read_and_insert()", []),
7983+
(f"call {table_name}_multi_insert()", []),
7984+
]
7985+
7986+
use_case[8] = use_case[6] + use_case[7]
7987+
use_case_exp[8] = use_case_exp[6] + use_case_exp[7]
7988+
7989+
use_case[9] = use_case[1] + use_case[5]
7990+
use_case_exp[9] = use_case_exp[1] + use_case_exp[5]
7991+
7992+
use_case[10] = use_case[3] + use_case[6]
7993+
use_case_exp[10] = use_case_exp[3] + use_case_exp[6]
7994+
7995+
use_case[
7996+
11
7997+
] = f"""
7998+
INSERT INTO {table_name} (city, country_id) VALUES ('Sonora', '33');
7999+
-- callproc invokes procedures internally;
8000+
CALL {table_name}_callproc();
8001+
"""
8002+
use_case_exp[11] = [
8003+
(
8004+
f"INSERT INTO {table_name} (city, country_id) VALUES ('Sonora', '33')",
8005+
[],
8006+
),
8007+
(f"CALL {table_name}_callproc()", [(1,)]),
8008+
(f"CALL {table_name}_callproc()", [(2,)]),
8009+
(f"CALL {table_name}_callproc()", [("bar",)]),
8010+
(f"CALL {table_name}_callproc()", []),
8011+
]
8012+
8013+
use_case[
8014+
12
8015+
] = f"""
8016+
INSERT INTO {table_name} (city, country_id) VALUES ('Tamaulipas', '82');
8017+
INSERT INTO {table_name} (city, country_id) VALUES ('Chihuahua', '3');
8018+
-- write operations;
8019+
SELECT ':D';
8020+
"""
8021+
use_case_exp[12] = [
8022+
(
8023+
f"INSERT INTO {table_name} (city, country_id) VALUES ('Tamaulipas', '82')",
8024+
[],
8025+
),
8026+
(
8027+
f"INSERT INTO {table_name} (city, country_id) VALUES ('Chihuahua', '3')",
8028+
[],
8029+
),
8030+
("SELECT ':D'", [(":D",)]),
8031+
]
8032+
8033+
use_case[13] = use_case[12] + use_case[11]
8034+
use_case_exp[13] = use_case_exp[12] + use_case_exp[11]
8035+
8036+
def setUp(self):
8037+
with connection.MySQLConnection(**tests.get_mysql_config()) as cnx:
8038+
cnx.cmd_query(f"DROP PROCEDURE IF EXISTS {self.table_name}_multi_read")
8039+
cnx.cmd_query(f"DROP PROCEDURE IF EXISTS {self.table_name}_multi_insert")
8040+
cnx.cmd_query(f"DROP PROCEDURE IF EXISTS {self.table_name}_single_read")
8041+
cnx.cmd_query(f"DROP PROCEDURE IF EXISTS {self.table_name}_read_and_insert")
8042+
cnx.cmd_query(f"DROP PROCEDURE IF EXISTS {self.table_name}_callproc")
8043+
cnx.cmd_query(f"DROP TABLE IF EXISTS {self.table_name}")
8044+
cnx.cmd_query(
8045+
f"""
8046+
CREATE PROCEDURE {self.table_name}_single_read(val integer)
8047+
BEGIN
8048+
SELECT val;
8049+
END;
8050+
"""
8051+
)
8052+
cnx.cmd_query(
8053+
f"""
8054+
CREATE PROCEDURE {self.table_name}_multi_read(val integer)
8055+
BEGIN
8056+
SELECT val;
8057+
SELECT val + 1;
8058+
SELECT 'bar';
8059+
END;
8060+
"""
8061+
)
8062+
cnx.cmd_query(
8063+
f"""
8064+
CREATE PROCEDURE {self.table_name}_multi_insert()
8065+
BEGIN
8066+
INSERT INTO {self.table_name} (city, country_id) VALUES ('Chiapas', '33');
8067+
INSERT INTO {self.table_name} (city, country_id) VALUES ('Yucatan', '28');
8068+
INSERT INTO {self.table_name} (city, country_id) VALUES ('Oaxaca', '13');
8069+
END;
8070+
"""
8071+
)
8072+
cnx.cmd_query(
8073+
f"""
8074+
CREATE PROCEDURE {self.table_name}_read_and_insert()
8075+
BEGIN
8076+
INSERT INTO {self.table_name} (city, country_id) VALUES ('CCC', '33');
8077+
SELECT 'Oracle';
8078+
INSERT INTO {self.table_name} (city, country_id) VALUES ('AAA', '44');
8079+
INSERT INTO {self.table_name} (city, country_id) VALUES ('BBB', '99');
8080+
SELECT 'MySQL';
8081+
END;
8082+
"""
8083+
)
8084+
cnx.cmd_query(
8085+
f"""
8086+
CREATE PROCEDURE {self.table_name}_callproc()
8087+
BEGIN
8088+
CALL {self.table_name}_multi_read(1);
8089+
CALL {self.table_name}_multi_insert();
8090+
END;
8091+
"""
8092+
)
8093+
cnx.cmd_query(
8094+
f"CREATE TABLE {self.table_name} (id INT AUTO_INCREMENT PRIMARY KEY, city VARCHAR(20),country_id INT)"
8095+
)
8096+
8097+
def tearDown(self) -> None:
8098+
with connection.MySQLConnection(**tests.get_mysql_config()) as cnx:
8099+
cnx.cmd_query(f"DROP PROCEDURE IF EXISTS {self.table_name}_multi_read")
8100+
cnx.cmd_query(f"DROP PROCEDURE IF EXISTS {self.table_name}_multi_insert")
8101+
cnx.cmd_query(f"DROP PROCEDURE IF EXISTS {self.table_name}_single_read")
8102+
cnx.cmd_query(f"DROP PROCEDURE IF EXISTS {self.table_name}_read_and_insert")
8103+
cnx.cmd_query(f"DROP PROCEDURE IF EXISTS {self.table_name}_callproc")
8104+
cnx.cmd_query(f"DROP TABLE IF EXISTS {self.table_name}")
8105+
8106+
@foreach_cnx()
8107+
def test_multi_stmts_with_callproc_out_of_sync(self):
8108+
with self.cnx.cursor() as cur:
8109+
for i in self.use_case:
8110+
for result, (stmt_exp, fetch_exp) in zip(
8111+
cur.execute(self.use_case[i], multi=True), self.use_case_exp[i]
8112+
):
8113+
fetch = result.fetchall()
8114+
self.assertEqual(result.statement, stmt_exp)
8115+
self.assertListEqual(fetch, fetch_exp)
8116+
8117+
8118+
class BugOra21390859_async(tests.MySQLConnectorAioTestCase):
8119+
"""BUG#21390859: STATEMENTS GET OUT OF SYNCH WITH RESULT SETS
8120+
8121+
For a description see `test_bugs.BugOra21390859`.
8122+
"""
8123+
8124+
def setUp(self) -> None:
8125+
self.bug_21390859 = BugOra21390859()
8126+
self.bug_21390859.setUp()
8127+
8128+
def tearDown(self) -> None:
8129+
self.bug_21390859.tearDown()
8130+
8131+
@foreach_cnx_aio()
8132+
async def test_multi_stmts_with_callproc_out_of_sync(self):
8133+
async with await self.cnx.cursor() as cur:
8134+
for i in self.bug_21390859.use_case:
8135+
exp_list = self.bug_21390859.use_case_exp[i]
8136+
j = 0
8137+
try:
8138+
async for result in cur.executemulti(self.bug_21390859.use_case[i]):
8139+
stmt_exp, fetch_exp = exp_list[j]
8140+
res = await result.fetchall()
8141+
self.assertEqual(result.statement, stmt_exp)
8142+
self.assertListEqual(res, fetch_exp)
8143+
j += 1
8144+
except IndexError:
8145+
self.fail(
8146+
"Got more results than expected "
8147+
f"for query {self.bug_21390859.use_case[i]}"
8148+
)

0 commit comments

Comments
 (0)