fix: accept prepare/binary kwargs in DictCursor.execute(..) (#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.
This commit is contained in:
Ashesh Vashi
2026-06-09 14:11:09 +05:30
committed by GitHub
parent d2bdd25160
commit ec3e6414e7
2 changed files with 70 additions and 6 deletions
+19 -6
View File
@@ -185,15 +185,21 @@ class DictCursor(_cursor):
self._ordered_description()
return self._odt_desc
def execute(self, query, params=None):
def execute(self, query, params=None, *, prepare=None, binary=None):
"""
Execute function
``prepare`` and ``binary`` are forwarded so this cursor stays
substitutable for ``psycopg.Cursor``. ``psycopg.Connection.execute``
passes ``prepare=...`` through to its underlying cursor; without
accepting it here that high-level call raises ``TypeError``.
"""
self._odt_desc = None
if params is not None and len(params) == 0:
params = None
return _cursor.execute(self, query, params)
return _cursor.execute(self, query, params,
prepare=prepare, binary=binary)
def fetchone(self):
"""
@@ -273,23 +279,30 @@ class AsyncDictCursor(_async_cursor):
self._ordered_description()
return self._odt_desc
def execute(self, query, params=None):
def execute(self, query, params=None, *, prepare=None, binary=None):
"""
Execute function
Mirrors ``DictCursor.execute`` so this cursor stays substitutable
for ``psycopg.AsyncCursor`` when used via ``AsyncConnection.execute``.
"""
try:
return asyncio.run(self._execute(query, params))
return asyncio.run(
self._execute(query, params, prepare=prepare, binary=binary)
)
except RuntimeError as e:
current_app.logger.exception(e)
async def _execute(self, query, params=None):
async def _execute(self, query, params=None, *,
prepare=None, binary=None):
"""
Execute function
"""
if params is not None and len(params) == 0:
params = None
return await self.cursor.execute(self, query, params)
return await self.cursor.execute(self, query, params,
prepare=prepare, binary=binary)
def executemany(self, query, params=None):
"""
@@ -0,0 +1,51 @@
##########################################################################
#
# pgAdmin 4 - PostgreSQL Tools
#
# Copyright (C) 2013 - 2026, The pgAdmin Development Team
# This software is released under the PostgreSQL Licence
#
##########################################################################
"""Regression test for DictCursor.execute() signature.
``psycopg.Cursor.execute`` exposes ``prepare`` and ``binary`` as keyword-only
parameters. For ``DictCursor`` (a ``psycopg.Cursor`` subclass) to remain
substitutable for the base cursor, its overridden ``execute`` must accept
those kwargs too.
The most visible failure mode is the ``Connection.execute`` path:
``psycopg.Connection.execute`` always forwards ``prepare=...`` to the
underlying cursor (``binary`` is handled by setting ``cur.format`` instead).
With a ``cursor_factory=DictCursor`` connection the forwarded ``prepare``
kwarg trips a narrowed ``DictCursor.execute`` signature with
``TypeError: execute() got an unexpected keyword argument 'prepare'``.
``binary`` doesn't break ``Connection.execute`` directly, but is asserted
here for full ``psycopg.Cursor`` signature parity.
"""
import inspect
from pgadmin.utils.driver.psycopg3.cursor import AsyncDictCursor, DictCursor
from pgadmin.utils.route import BaseTestGenerator
class TestDictCursorExecuteSignature(BaseTestGenerator):
"""Verify (Async)DictCursor.execute exposes ``prepare`` and ``binary``."""
scenarios = [
('DictCursor.execute accepts prepare/binary',
dict(cls=DictCursor)),
('AsyncDictCursor.execute accepts prepare/binary',
dict(cls=AsyncDictCursor)),
]
def runTest(self):
params = inspect.signature(self.cls.execute).parameters
self.assertIn('prepare', params)
self.assertIn('binary', params)
# Must be keyword-only — psycopg passes them as kwargs.
self.assertEqual(params['prepare'].kind,
inspect.Parameter.KEYWORD_ONLY)
self.assertEqual(params['binary'].kind,
inspect.Parameter.KEYWORD_ONLY)