Skip to content

Commit c6893e9

Browse files
committed
Document pgbouncer-related prepared statement breakage
Fixes: #121, #149.
1 parent f4e17dd commit c6893e9

File tree

7 files changed

+125
-28
lines changed

7 files changed

+125
-28
lines changed

.github/ISSUE_TEMPLATE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Thank you!
1212

1313
* **asyncpg version**:
1414
* **PostgreSQL version**:
15+
* **Do you use a PostgreSQL SaaS? If so, which? Can you reproduce
16+
the issue with a local PostgreSQL install?**:
1517
* **Python version**:
1618
* **Platform**:
1719
* **Do you use pgbouncer?**:

asyncpg/exceptions/_base.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import asyncpg
99
import sys
10+
import textwrap
1011

1112

1213
__all__ = ('PostgresError', 'FatalPostgresError', 'UnknownPostgresError',
@@ -121,6 +122,31 @@ def _make_constructor(cls, fields, query=None):
121122
message = ('cached statement plan is invalid due to a database '
122123
'schema or configuration change')
123124

125+
is_prepared_stmt_error = (
126+
exccls.__name__ in ('DuplicatePreparedStatementError',
127+
'InvalidSQLStatementNameError') and
128+
_is_asyncpg_class(exccls)
129+
)
130+
131+
if is_prepared_stmt_error:
132+
hint = dct.get('hint', '')
133+
hint += textwrap.dedent("""\
134+
135+
NOTE: pgbouncer with pool_mode set to "transaction" or
136+
"statement" does not support prepared statements properly.
137+
You have two options:
138+
139+
* if you are using pgbouncer for connection pooling to a
140+
single server, switch to the connection pool functionality
141+
provided by asyncpg, it is a much better option for this
142+
purpose;
143+
144+
* if you have no option of avoiding the use of pgbouncer,
145+
then you must switch pgbouncer's pool_mode to "session".
146+
""")
147+
148+
dct['hint'] = hint
149+
124150
return exccls, message, dct
125151

126152
def as_dict(self):

asyncpg/transaction.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ async def start(self):
117117

118118
try:
119119
await self._connection.execute(query)
120-
except:
120+
except BaseException:
121121
self._state = TransactionState.FAILED
122122
raise
123123
else:
@@ -158,7 +158,7 @@ async def __commit(self):
158158

159159
try:
160160
await self._connection.execute(query)
161-
except:
161+
except BaseException:
162162
self._state = TransactionState.FAILED
163163
raise
164164
else:
@@ -177,7 +177,7 @@ async def __rollback(self):
177177

178178
try:
179179
await self._connection.execute(query)
180-
except:
180+
except BaseException:
181181
self._state = TransactionState.FAILED
182182
raise
183183
else:

docs/api/index.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ a need to run the same query again.
5353
during calls to the :meth:`~Connection.fetch`, :meth:`~Connection.fetchrow`,
5454
or :meth:`~Connection.fetchval` methods.
5555

56+
.. warning::
57+
58+
If you are using pgbouncer with ``pool_mode`` set to ``transaction`` or
59+
``statement``, prepared statements will not work correctly. See
60+
:ref:`asyncpg-prepared-stmt-errors` for more information.
61+
5662

5763
.. autoclass:: asyncpg.prepared_stmt.PreparedStatement()
5864
:members:

docs/faq.rst

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,73 @@ Frequently Asked Questions
55
==========================
66

77
Does asyncpg support DB-API?
8-
No. DB-API is a synchronous API, while asyncpg is based
9-
around an asynchronous I/O model. Thus, full drop-in compatibility
10-
with DB-API is not possible and we decided to design asyncpg API
11-
in a way that is better aligned with PostgreSQL architecture and
12-
terminology. We will release a synchronous DB-API-compatible version
13-
of asyncpg at some point in the future.
8+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
9+
10+
No. DB-API is a synchronous API, while asyncpg is based
11+
around an asynchronous I/O model. Thus, full drop-in compatibility
12+
with DB-API is not possible and we decided to design asyncpg API
13+
in a way that is better aligned with PostgreSQL architecture and
14+
terminology. We will release a synchronous DB-API-compatible version
15+
of asyncpg at some point in the future.
16+
1417

1518
Can I use asyncpg with SQLAlchemy ORM?
16-
Short answer: no. asyncpg uses asynchronous execution model
17-
and API, which is fundamentally incompatible with SQLAlchemy.
18-
However, it is possible to use asyncpg and SQLAlchemy Core
19-
with the help of a third-party adapter, such as asyncpgsa_.
19+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
20+
21+
Short answer: no. asyncpg uses asynchronous execution model
22+
and API, which is fundamentally incompatible with SQLAlchemy.
23+
However, it is possible to use asyncpg and SQLAlchemy Core
24+
with the help of a third-party adapter, such as asyncpgsa_.
25+
2026

2127
Can I use dot-notation with :class:`asyncpg.Record`? It looks cleaner.
22-
We decided against making :class:`asyncpg.Record` a named tuple
23-
because we want to keep the ``Record`` method namespace separate
24-
from the column namespace.
28+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
29+
30+
We decided against making :class:`asyncpg.Record` a named tuple
31+
because we want to keep the ``Record`` method namespace separate
32+
from the column namespace.
33+
2534

2635
Why can't I use a :ref:`cursor <asyncpg-api-cursor>` outside of a transaction?
27-
Cursors created by a call to
28-
:meth:`Connection.cursor() <asyncpg.connection.Connection.cursor>` or
29-
:meth:`PreparedStatement.cursor() \
30-
<asyncpg.prepared_stmt.PreparedStatement.cursor>`
31-
cannot be used outside of a transaction. Any such attempt will result in
32-
``InterfaceError``.
33-
To create a cursor usable outside of a transaction, use the
34-
``DECLARE ... CURSOR WITH HOLD`` SQL statement directly.
36+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
37+
38+
Cursors created by a call to
39+
:meth:`Connection.cursor() <asyncpg.connection.Connection.cursor>` or
40+
:meth:`PreparedStatement.cursor() \
41+
<asyncpg.prepared_stmt.PreparedStatement.cursor>`
42+
cannot be used outside of a transaction. Any such attempt will result in
43+
``InterfaceError``.
44+
To create a cursor usable outside of a transaction, use the
45+
``DECLARE ... CURSOR WITH HOLD`` SQL statement directly.
46+
47+
48+
.. _asyncpg-prepared-stmt-errors:
49+
50+
Why am I getting prepared statement errors?
51+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
52+
53+
If you are getting intermittent ``prepared statement "__asyncpg_stmt_xx__"
54+
does not exist`` or ``prepared statement “__asyncpg_stmt_xx__”
55+
already exists`` errors, you are most likely not connecting to the
56+
PostgreSQL server directly, but via
57+
`pgbouncer <https://pgbouncer.github.io/>`_. pgbouncer, when
58+
in the ``"transaction"`` or ``"statement"`` pooling mode, does not support
59+
prepared statements. You have two options:
60+
61+
* if you are using pgbouncer for connection pooling to a single server,
62+
switch to the :ref:`connection pool <asyncpg-connection-pool>`
63+
functionality provided by asyncpg, it is a much better option for this
64+
purpose;
65+
66+
* if you have no option of avoiding the use of pgbouncer, then you need to
67+
switch pgbouncer's ``pool_mode`` to ``session``.
68+
3569

3670
Why do I get ``PostgresSyntaxError`` when using ``expression IN $1``?
37-
``expression IN $1`` is not a valid PostgreSQL syntax. To check
38-
a value against a sequence use ``expression = any($1::mytype[])``,
39-
where ``mytype`` is the array element type.
71+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
72+
73+
``expression IN $1`` is not a valid PostgreSQL syntax. To check
74+
a value against a sequence use ``expression = any($1::mytype[])``,
75+
where ``mytype`` is the array element type.
4076

4177
.. _asyncpgsa: https://github.com/CanopyTax/asyncpgsa

docs/usage.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,8 @@ The most common way to use transactions is through an ``async with`` statement:
222222
See the :ref:`asyncpg-api-transaction` API documentation for more information.
223223

224224

225+
.. _asyncpg-connection-pool:
226+
225227
Connection Pools
226228
----------------
227229

@@ -232,7 +234,7 @@ pool implementation, which eliminates the need to use an external connection
232234
pooler such as PgBouncer.
233235

234236
To create a connection pool, use the
235-
:func:`asyncpg.create_pool <asyncpg.pool.create_pool>` function.
237+
:func:`asyncpg.create_pool() <asyncpg.pool.create_pool>` function.
236238
The resulting :class:`Pool <asyncpg.pool.Pool>` object can then be used
237239
to borrow connections from the pool.
238240

tests/test_prepare.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,3 +569,28 @@ async def test_prepare_30_invalid_arg_count(self):
569569
exceptions.InterfaceError,
570570
'the server expects 0 arguments for this query, 1 was passed'):
571571
await self.con.fetchval('SELECT 1', 1)
572+
573+
async def test_prepare_31_pgbouncer_note(self):
574+
try:
575+
await self.con.execute("""
576+
DO $$ BEGIN
577+
RAISE EXCEPTION
578+
'duplicate statement' USING ERRCODE = '42P05';
579+
END; $$ LANGUAGE plpgsql;
580+
""")
581+
except asyncpg.DuplicatePreparedStatementError as e:
582+
self.assertTrue('pgbouncer' in e.hint)
583+
else:
584+
self.fail('DuplicatePreparedStatementError not raised')
585+
586+
try:
587+
await self.con.execute("""
588+
DO $$ BEGIN
589+
RAISE EXCEPTION
590+
'invalid statement' USING ERRCODE = '26000';
591+
END; $$ LANGUAGE plpgsql;
592+
""")
593+
except asyncpg.InvalidSQLStatementNameError as e:
594+
self.assertTrue('pgbouncer' in e.hint)
595+
else:
596+
self.fail('InvalidSQLStatementNameError not raised')

0 commit comments

Comments
 (0)