mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2026-08-17 16:34:44 -05:00
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:
@@ -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)
|
||||
Reference in New Issue
Block a user