Skip to content

Commit ec3e641

Browse files
authored
fix: accept prepare/binary kwargs in DictCursor.execute(..) (pgadmin-org#10030)
psycopg.Connection.execute(query, params, *, prepare=None, binary=False) delegates to its underlying cursor as cur.execute(query, params, prepare=prepare). The DictCursor / AsyncDictCursor in pgadmin.utils.driver.psycopg3.cursor narrowed the signature to (self, query, params=None), so any caller that uses the high-level Connection.execute() path with a cursor_factory=DictCursor connection hits TypeError: execute() got an unexpected keyword argument 'prepare' The most visible victim is psycopg_pool.ConnectionPool.check_connection, which sends conn.execute("") to validate every checkout — every connection then looks broken to the pool and getconn() times out. Forward prepare and binary (keyword-only) to the underlying psycopg.Cursor / psycopg.AsyncCursor. Defaults match psycopg's own, so existing callers see no behavior change. Adds a regression test asserting both classes expose the kwargs as keyword-only parameters. The test docstring leads with psycopg.Cursor substitutability (DictCursor is a Cursor subclass; both kwargs must be accepted to remain substitutable) and notes that Connection.execute is just the most visible failure path — it forwards prepare but handles binary by setting cur.format instead of forwarding it.
1 parent d2bdd25 commit ec3e641

2 files changed

Lines changed: 70 additions & 6 deletions

File tree

web/pgadmin/utils/driver/psycopg3/cursor.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,15 +185,21 @@ def ordered_description(self):
185185
self._ordered_description()
186186
return self._odt_desc
187187

188-
def execute(self, query, params=None):
188+
def execute(self, query, params=None, *, prepare=None, binary=None):
189189
"""
190190
Execute function
191+
192+
``prepare`` and ``binary`` are forwarded so this cursor stays
193+
substitutable for ``psycopg.Cursor``. ``psycopg.Connection.execute``
194+
passes ``prepare=...`` through to its underlying cursor; without
195+
accepting it here that high-level call raises ``TypeError``.
191196
"""
192197
self._odt_desc = None
193198
if params is not None and len(params) == 0:
194199
params = None
195200

196-
return _cursor.execute(self, query, params)
201+
return _cursor.execute(self, query, params,
202+
prepare=prepare, binary=binary)
197203

198204
def fetchone(self):
199205
"""
@@ -273,23 +279,30 @@ def ordered_description(self):
273279
self._ordered_description()
274280
return self._odt_desc
275281

276-
def execute(self, query, params=None):
282+
def execute(self, query, params=None, *, prepare=None, binary=None):
277283
"""
278284
Execute function
285+
286+
Mirrors ``DictCursor.execute`` so this cursor stays substitutable
287+
for ``psycopg.AsyncCursor`` when used via ``AsyncConnection.execute``.
279288
"""
280289
try:
281-
return asyncio.run(self._execute(query, params))
290+
return asyncio.run(
291+
self._execute(query, params, prepare=prepare, binary=binary)
292+
)
282293
except RuntimeError as e:
283294
current_app.logger.exception(e)
284295

285-
async def _execute(self, query, params=None):
296+
async def _execute(self, query, params=None, *,
297+
prepare=None, binary=None):
286298
"""
287299
Execute function
288300
"""
289301
if params is not None and len(params) == 0:
290302
params = None
291303

292-
return await self.cursor.execute(self, query, params)
304+
return await self.cursor.execute(self, query, params,
305+
prepare=prepare, binary=binary)
293306

294307
def executemany(self, query, params=None):
295308
"""
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
##########################################################################
2+
#
3+
# pgAdmin 4 - PostgreSQL Tools
4+
#
5+
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
6+
# This software is released under the PostgreSQL Licence
7+
#
8+
##########################################################################
9+
10+
"""Regression test for DictCursor.execute() signature.
11+
12+
``psycopg.Cursor.execute`` exposes ``prepare`` and ``binary`` as keyword-only
13+
parameters. For ``DictCursor`` (a ``psycopg.Cursor`` subclass) to remain
14+
substitutable for the base cursor, its overridden ``execute`` must accept
15+
those kwargs too.
16+
17+
The most visible failure mode is the ``Connection.execute`` path:
18+
``psycopg.Connection.execute`` always forwards ``prepare=...`` to the
19+
underlying cursor (``binary`` is handled by setting ``cur.format`` instead).
20+
With a ``cursor_factory=DictCursor`` connection the forwarded ``prepare``
21+
kwarg trips a narrowed ``DictCursor.execute`` signature with
22+
``TypeError: execute() got an unexpected keyword argument 'prepare'``.
23+
``binary`` doesn't break ``Connection.execute`` directly, but is asserted
24+
here for full ``psycopg.Cursor`` signature parity.
25+
"""
26+
27+
import inspect
28+
29+
from pgadmin.utils.driver.psycopg3.cursor import AsyncDictCursor, DictCursor
30+
from pgadmin.utils.route import BaseTestGenerator
31+
32+
33+
class TestDictCursorExecuteSignature(BaseTestGenerator):
34+
"""Verify (Async)DictCursor.execute exposes ``prepare`` and ``binary``."""
35+
36+
scenarios = [
37+
('DictCursor.execute accepts prepare/binary',
38+
dict(cls=DictCursor)),
39+
('AsyncDictCursor.execute accepts prepare/binary',
40+
dict(cls=AsyncDictCursor)),
41+
]
42+
43+
def runTest(self):
44+
params = inspect.signature(self.cls.execute).parameters
45+
self.assertIn('prepare', params)
46+
self.assertIn('binary', params)
47+
# Must be keyword-only — psycopg passes them as kwargs.
48+
self.assertEqual(params['prepare'].kind,
49+
inspect.Parameter.KEYWORD_ONLY)
50+
self.assertEqual(params['binary'].kind,
51+
inspect.Parameter.KEYWORD_ONLY)

0 commit comments

Comments
 (0)