From 7eb5d552f55f99750b1f2315ec9a4e045b2cb969 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Fri, 16 Oct 2026 04:21:29 -0400 Subject: [PATCH] fix: layer-1 verbs always substitute params, so a % identifier reaches the server intact T3-16: _norm_params turned an empty params list into None, so aiomysql skipped query % args and a no-condition get/get_one/exists/delete on a table named 50%_off sent `50%%_off` (table not found). layer 1 now passes its params list as-is; only the layer-2 methods normalize. Signed-off-by: disqualifier --- src/mysql/mysql.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/mysql/mysql.py b/src/mysql/mysql.py index d8386c1..20c359f 100644 --- a/src/mysql/mysql.py +++ b/src/mysql/mysql.py @@ -198,18 +198,20 @@ class MysqlDB: async def _run(self, query: str, params: Optional[Sequence] = None): """execute a statement on a pooled cursor; return (rowcount, lastrowid) - params go through `_norm_params` (see it for the no-substitution/str-bytes handling) + params are passed to aiomysql as-is: layer 1 always hands a list (even empty) so + `query % args` runs and collapses `_quote_ident`'s `%%`; layer 2 normalizes first + via `_norm_params` """ async with self.pool.acquire() as conn: async with conn.cursor() as cur: - await cur.execute(query, _norm_params(params)) + await cur.execute(query, params) return cur.rowcount, cur.lastrowid async def _fetchall(self, query: str, params: Optional[Sequence] = None) -> List[dict]: """execute a query and return all rows as dicts""" async with self.pool.acquire() as conn: async with conn.cursor() as cur: - await cur.execute(query, _norm_params(params)) + await cur.execute(query, params) rows = await cur.fetchall() return list(rows) @@ -217,7 +219,7 @@ class MysqlDB: """execute a query and return the first row dict, or None""" async with self.pool.acquire() as conn: async with conn.cursor() as cur: - await cur.execute(query, _norm_params(params)) + await cur.execute(query, params) return await cur.fetchone() # ------------------------------------------------------------------------- @@ -230,7 +232,7 @@ class MysqlDB: lastrowid, or reach for the raw pool. """ try: - rowcount, _ = await self._run(query, params) + rowcount, _ = await self._run(query, _norm_params(params)) except _DRIVER_ERRORS: log.debug("mysql.execute(): %s", query, exc_info=True) raise @@ -239,7 +241,7 @@ class MysqlDB: async def fetch(self, query: str, params: Optional[Sequence] = None) -> List[dict]: """run a query and return all rows as plain dicts (empty list = no rows)""" try: - return await self._fetchall(query, params) + return await self._fetchall(query, _norm_params(params)) except _DRIVER_ERRORS: log.debug("mysql.fetch(): %s", query, exc_info=True) raise @@ -247,7 +249,7 @@ class MysqlDB: async def fetchone(self, query: str, params: Optional[Sequence] = None) -> Optional[dict]: """run a query and return the first row as a dict, or None if no rows""" try: - return await self._fetchone(query, params) + return await self._fetchone(query, _norm_params(params)) except _DRIVER_ERRORS: log.debug("mysql.fetchone(): %s", query, exc_info=True) raise @@ -255,7 +257,7 @@ class MysqlDB: async def fetchval(self, query: str, params: Optional[Sequence] = None) -> Any: """run a query and return the first column of the first row, or None""" try: - row = await self._fetchone(query, params) + row = await self._fetchone(query, _norm_params(params)) except _DRIVER_ERRORS: log.debug("mysql.fetchval(): %s", query, exc_info=True) raise