Skip to content

Document pgbouncer-related prepared statement breakage #214

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Thank you!

* **asyncpg version**:
* **PostgreSQL version**:
* **Do you use a PostgreSQL SaaS? If so, which? Can you reproduce
the issue with a local PostgreSQL install?**:
* **Python version**:
* **Platform**:
* **Do you use pgbouncer?**:
Expand Down
26 changes: 26 additions & 0 deletions asyncpg/exceptions/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import asyncpg
import sys
import textwrap


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

is_prepared_stmt_error = (
exccls.__name__ in ('DuplicatePreparedStatementError',
'InvalidSQLStatementNameError') and
_is_asyncpg_class(exccls)
)

if is_prepared_stmt_error:
hint = dct.get('hint', '')
hint += textwrap.dedent("""\

NOTE: pgbouncer with pool_mode set to "transaction" or
"statement" does not support prepared statements properly.
You have two options:

* if you are using pgbouncer for connection pooling to a
single server, switch to the connection pool functionality
provided by asyncpg, it is a much better option for this
purpose;

* if you have no option of avoiding the use of pgbouncer,
then you must switch pgbouncer's pool_mode to "session".
""")

dct['hint'] = hint

return exccls, message, dct

def as_dict(self):
Expand Down
6 changes: 3 additions & 3 deletions asyncpg/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ async def start(self):

try:
await self._connection.execute(query)
except:
except BaseException:
self._state = TransactionState.FAILED
raise
else:
Expand Down Expand Up @@ -158,7 +158,7 @@ async def __commit(self):

try:
await self._connection.execute(query)
except:
except BaseException:
self._state = TransactionState.FAILED
raise
else:
Expand All @@ -177,7 +177,7 @@ async def __rollback(self):

try:
await self._connection.execute(query)
except:
except BaseException:
self._state = TransactionState.FAILED
raise
else:
Expand Down
6 changes: 6 additions & 0 deletions docs/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ a need to run the same query again.
during calls to the :meth:`~Connection.fetch`, :meth:`~Connection.fetchrow`,
or :meth:`~Connection.fetchval` methods.

.. warning::

If you are using pgbouncer with ``pool_mode`` set to ``transaction`` or
``statement``, prepared statements will not work correctly. See
:ref:`asyncpg-prepared-stmt-errors` for more information.


.. autoclass:: asyncpg.prepared_stmt.PreparedStatement()
:members:
Expand Down
84 changes: 60 additions & 24 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,73 @@ Frequently Asked Questions
==========================

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

No. DB-API is a synchronous API, while asyncpg is based
around an asynchronous I/O model. Thus, full drop-in compatibility
with DB-API is not possible and we decided to design asyncpg API
in a way that is better aligned with PostgreSQL architecture and
terminology. We will release a synchronous DB-API-compatible version
of asyncpg at some point in the future.


Can I use asyncpg with SQLAlchemy ORM?
Short answer: no. asyncpg uses asynchronous execution model
and API, which is fundamentally incompatible with SQLAlchemy.
However, it is possible to use asyncpg and SQLAlchemy Core
with the help of a third-party adapter, such as asyncpgsa_.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Short answer: no. asyncpg uses asynchronous execution model
and API, which is fundamentally incompatible with SQLAlchemy.
However, it is possible to use asyncpg and SQLAlchemy Core
with the help of a third-party adapter, such as asyncpgsa_.


Can I use dot-notation with :class:`asyncpg.Record`? It looks cleaner.
We decided against making :class:`asyncpg.Record` a named tuple
because we want to keep the ``Record`` method namespace separate
from the column namespace.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

We decided against making :class:`asyncpg.Record` a named tuple
because we want to keep the ``Record`` method namespace separate
from the column namespace.


Why can't I use a :ref:`cursor <asyncpg-api-cursor>` outside of a transaction?
Cursors created by a call to
:meth:`Connection.cursor() <asyncpg.connection.Connection.cursor>` or
:meth:`PreparedStatement.cursor() \
<asyncpg.prepared_stmt.PreparedStatement.cursor>`
cannot be used outside of a transaction. Any such attempt will result in
``InterfaceError``.
To create a cursor usable outside of a transaction, use the
``DECLARE ... CURSOR WITH HOLD`` SQL statement directly.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Cursors created by a call to
:meth:`Connection.cursor() <asyncpg.connection.Connection.cursor>` or
:meth:`PreparedStatement.cursor() \
<asyncpg.prepared_stmt.PreparedStatement.cursor>`
cannot be used outside of a transaction. Any such attempt will result in
``InterfaceError``.
To create a cursor usable outside of a transaction, use the
``DECLARE ... CURSOR WITH HOLD`` SQL statement directly.


.. _asyncpg-prepared-stmt-errors:

Why am I getting prepared statement errors?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If you are getting intermittent ``prepared statement "__asyncpg_stmt_xx__"
does not exist`` or ``prepared statement “__asyncpg_stmt_xx__”
already exists`` errors, you are most likely not connecting to the
PostgreSQL server directly, but via
`pgbouncer <https://pgbouncer.github.io/>`_. pgbouncer, when
in the ``"transaction"`` or ``"statement"`` pooling mode, does not support
prepared statements. You have two options:

* if you are using pgbouncer for connection pooling to a single server,
switch to the :ref:`connection pool <asyncpg-connection-pool>`
functionality provided by asyncpg, it is a much better option for this
purpose;

* if you have no option of avoiding the use of pgbouncer, then you need to
switch pgbouncer's ``pool_mode`` to ``session``.


Why do I get ``PostgresSyntaxError`` when using ``expression IN $1``?
``expression IN $1`` is not a valid PostgreSQL syntax. To check
a value against a sequence use ``expression = any($1::mytype[])``,
where ``mytype`` is the array element type.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

``expression IN $1`` is not a valid PostgreSQL syntax. To check
a value against a sequence use ``expression = any($1::mytype[])``,
where ``mytype`` is the array element type.

.. _asyncpgsa: https://github.com/CanopyTax/asyncpgsa
4 changes: 3 additions & 1 deletion docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ The most common way to use transactions is through an ``async with`` statement:
See the :ref:`asyncpg-api-transaction` API documentation for more information.


.. _asyncpg-connection-pool:

Connection Pools
----------------

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

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

Expand Down
25 changes: 25 additions & 0 deletions tests/test_prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,3 +569,28 @@ async def test_prepare_30_invalid_arg_count(self):
exceptions.InterfaceError,
'the server expects 0 arguments for this query, 1 was passed'):
await self.con.fetchval('SELECT 1', 1)

async def test_prepare_31_pgbouncer_note(self):
try:
await self.con.execute("""
DO $$ BEGIN
RAISE EXCEPTION
'duplicate statement' USING ERRCODE = '42P05';
END; $$ LANGUAGE plpgsql;
""")
except asyncpg.DuplicatePreparedStatementError as e:
self.assertTrue('pgbouncer' in e.hint)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add else: self.fail('...')

else:
self.fail('DuplicatePreparedStatementError not raised')

try:
await self.con.execute("""
DO $$ BEGIN
RAISE EXCEPTION
'invalid statement' USING ERRCODE = '26000';
END; $$ LANGUAGE plpgsql;
""")
except asyncpg.InvalidSQLStatementNameError as e:
self.assertTrue('pgbouncer' in e.hint)
else:
self.fail('InvalidSQLStatementNameError not raised')