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 <dev@disqualifier.me>
This commit is contained in:
dsql committed 2026-10-16 04:21:29 -04:00
1 parent febace7d5d
commit 7eb5d552f5
1 file changed
+10 -8
+10 -8
View File
@@ -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