From: Daniele Varrazzo Date: Wed, 18 Nov 2020 15:05:35 +0000 (+0000) Subject: Added documentation for transaction management X-Git-Tag: 3.0.dev0~351^2~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=167bd50d387c21afe834fe8e3d1531963540649a;p=thirdparty%2Fpsycopg.git Added documentation for transaction management --- diff --git a/docs/_static/.keep b/docs/_static/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/_static/psycopg.css b/docs/_static/psycopg.css new file mode 100644 index 000000000..316297865 --- /dev/null +++ b/docs/_static/psycopg.css @@ -0,0 +1,4 @@ +.hint { + background: #ffc; + border: 1px solid #dda; +} diff --git a/docs/connection.rst b/docs/connection.rst index d0317823f..03cb4441f 100644 --- a/docs/connection.rst +++ b/docs/connection.rst @@ -29,8 +29,6 @@ The `!Connection` class transaction will be committed (or rolled back, in case of exception) and the connection will be closed. - .. rubric:: Methods you will need every day - .. automethod:: connect Connection parameters can be passed either as a `conninfo string`__ (a @@ -50,26 +48,41 @@ The `!Connection` class .. __: https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-PARAMKEYWORDS .. __: https://www.postgresql.org/docs/current/libpq-envars.html + .. automethod:: close .. automethod:: cursor - .. automethod:: commit - .. automethod:: rollback - .. autoattribute:: autocommit - The property is writable for sync connections, read-only for async - ones: you can call `~AsyncConnection.set_autocommit()` on those. + .. rubric:: Transaction management methods For details see :ref:`transactions`. - .. automethod:: close + .. automethod:: commit() + .. automethod:: rollback() + .. automethod:: transaction(savepoint_name: Optional[str] = None, force_rollback: bool = False) -> Transaction + + It must be called as ``with conn.transaction() as tx: ...`` + + Inside a transaction block it will not be possible to call `commit()` + or `rollback()`. + + .. autoattribute:: autocommit + :annotation: bool + + The property is writable for sync connections, read-only for async + ones: you should call `!await` `~AsyncConnection.set_autocommit`\ + :samp:`({value})` instead. .. rubric:: Checking and configuring the connection state - .. autoproperty:: closed - .. autoproperty:: client_encoding + .. autoattribute:: closed + :annotation: bool + + .. autoattribute:: client_encoding + :annotation: str The property is writable for sync connections, read-only for async - ones: you can call `~AsyncConnection.set_client_encoding()` on those. + ones: you should call `!await` `~AsyncConnection.set_client_encoding`\ + :samp:`({value})` instead. .. attribute:: info @@ -112,6 +125,11 @@ The `!AsyncConnection` class .. automethod:: cursor .. automethod:: commit .. automethod:: rollback + + .. automethod:: transaction(savepoint_name: Optional[str] = None, force_rollback: bool = False) -> AsyncTransaction + + It must be called as ``async with conn.transaction() as tx: ...``. + .. automethod:: notifies .. automethod:: set_client_encoding .. automethod:: set_autocommit @@ -122,3 +140,11 @@ Connection support objects .. autoclass:: Notify :members: channel, payload, pid + +.. autoclass:: Transaction(connection: Connection, savepoint_name: Optional[str] = None, force_rollback: bool = False) + + .. autoproperty:: savepoint_name + .. autoattribute:: connection + :annotation: Connection + +.. autoclass:: AsyncTransaction(connection: AsyncConnection, savepoint_name: Optional[str] = None, force_rollback: bool = False) diff --git a/docs/usage.rst b/docs/usage.rst index d4872a5c6..fc24d3187 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -136,17 +136,106 @@ TODO: lift from psycopg2 docs Binary parameters and results ----------------------------- -TODO: lift from psycopg2 docs +TODO +.. index:: Transactions management +.. index:: InFailedSqlTransaction +.. index:: idle in transaction .. _transactions: Transaction management ----------------------- +====================== -TODO +`!psycopg3` has a behaviour that may result surprising compared to +:program:`psql`: by default, any database operation will start a new +transaction. As a consequence, changes made by any cursor of the connection +will not be visible until `Connection.commit()` is called, and will be +discarded by `Connection.rollback()`. The following operation on the same +connection will start a new transaction. + +If a database operation fails, the server will refuse further commands, until +a `~rollback()` is called. + +.. hint:: + + If a database operation fails with an error message such as + *InFailedSqlTransaction: current transaction is aborted, commands ignored + until end of transaction block*, it means that **a previous operation + failed** and the database session is in a state on error. You need to call + `!rollback()` if you want to keep on using the same connection. + +The manual commit requirement can be suspended using `~Connection.autocommit`, +either as connection attribute or as `~psycopg3.Connection.connect()` +parameter. This may be required to run operations that need to run outside a +transaction, such as :sql:`CREATE DATABASE`, :sql:`VACUUM`, :sql:`CALL` on +`stored procedures`__ using transaction control. + +.. __: https://www.postgresql.org/docs/current/xproc.html + +.. warning:: + + By default even a simple :sql:`SELECT` will start a transaction: in + long-running programs, if no further action is taken, the session will + remain *idle in transaction*, an undesirable condition for several + reasons (locks are held by the session, tables bloat...). For long lived + scripts, either make sure to terminate a transaction as soon as possible or + use an `~Connection.autocommit` connection. + + +.. _transaction-block: + +Transaction blocks +------------------ + +A more transparent way to make sure that transactions are finalised at the +right time is to use `!with` `Connection.transaction()` to create a +transaction block. When the block is entered a transaction is started; when +leaving the block the transaction is committed, or it is rolled back if an +exception is raised inside the block. + +For instance, an hypothetical but extremely secure bank may have the following +code to avoid that no accident between the following two lines leaves the +accounts unbalanced: + +.. code:: python + + with conn.transaction(): + move_money(conn, account1, -100) + move_money(conn, account2, +100) + + # The transaction is now committed + +Transaction blocks can also be nested (internal transaction blocks are +implemented using :sql:`SAVEPOINT`): an exception raised inside an inner block +has a chance of being handled and not fail completely outer operations. The +following is an example where a series of operation interact with the +database. Operations are allowed to fail, and we want to store the number of +operations successfully processed too. + +.. code:: python + + with conn.transaction() as tx1: + num_ok = 0 + for operation in operations: + try: + with conn.transaction() as tx2: + unreliable_operation(conn, operation) + except Exception: + logger.exception(f"{operation} failed") + else: + num_ok += 1 + + save_number_of_successes(conn, num_ok) + +If `!unreliable_operation()` causes an error, including an operation causing a +database error, all its changes will be reverted. The exception bubbles up +outside the block: in the example it is intercepted by the `!try` so that the +loop can complete. The outermost loop is unaffected (unless other errors +happen there). +.. TODO: Document Rollback or remove it .. index:: @@ -155,7 +244,7 @@ TODO .. _copy: Using COPY TO and COPY FROM ---------------------------- +=========================== `psycopg3` allows to operate with `PostgreSQL COPY protocol`__. :sql:`COPY` is one of the most efficient ways to load data into the database (and to modify @@ -224,7 +313,7 @@ Binary data can be produced and consumed using :sql:`FORMAT BINARY` in the :sql:`COPY` command: see :ref:`binary-data` for details and limitations. -.. index:: async +.. index:: asyncio Async operations ================ @@ -233,7 +322,7 @@ psycopg3 `~Connection` and `~Cursor` have counterparts `~AsyncConnection` and `~AsyncCursor` supporting an `asyncio` interface. The design of the asynchronous objects is pretty much the same of the sync -ones: in order to use them you will only have to scatter the ``async`` keyword +ones: in order to use them you will only have to scatter the ``await`` keyword here and there. .. code:: python diff --git a/psycopg3/psycopg3/__init__.py b/psycopg3/psycopg3/__init__.py index 6f6a907e2..13067887d 100644 --- a/psycopg3/psycopg3/__init__.py +++ b/psycopg3/psycopg3/__init__.py @@ -11,7 +11,7 @@ from .errors import Warning, Error, InterfaceError, DatabaseError from .errors import DataError, OperationalError, IntegrityError from .errors import InternalError, ProgrammingError, NotSupportedError from .connection import AsyncConnection, Connection, Notify -from .transaction import Rollback +from .transaction import Rollback, Transaction, AsyncTransaction from .dbapi20 import BINARY, DATETIME, NUMBER, ROWID, STRING from .dbapi20 import Binary, Date, DateFromTicks, Time, TimeFromTicks diff --git a/psycopg3/psycopg3/connection.py b/psycopg3/psycopg3/connection.py index 2dd262de4..e308b6b92 100644 --- a/psycopg3/psycopg3/connection.py +++ b/psycopg3/psycopg3/connection.py @@ -341,6 +341,14 @@ class Connection(BaseConnection): savepoint_name: Optional[str] = None, force_rollback: bool = False, ) -> Iterator[Transaction]: + """ + Start a context block with a new transaction or nested transaction. + + :param savepoint_name: Name of the savepoint used to manage a nested + transaction. If `!None`, one will be chosen automatically. + :param force_rollback: Roll back the transaction at the end of the + block even if there were no error (e.g. to try a no-op process). + """ with Transaction(self, savepoint_name, force_rollback) as tx: yield tx @@ -489,6 +497,9 @@ class AsyncConnection(BaseConnection): savepoint_name: Optional[str] = None, force_rollback: bool = False, ) -> AsyncIterator[AsyncTransaction]: + """ + Start a context block with a new transaction or nested transaction. + """ tx = AsyncTransaction(self, savepoint_name, force_rollback) async with tx: yield tx @@ -504,7 +515,7 @@ class AsyncConnection(BaseConnection): ) async def set_client_encoding(self, name: str) -> None: - """Async version of the `client_encoding` setter.""" + """Async version of the `~Connection.client_encoding` setter.""" async with self.lock: self.pgconn.send_query_params( b"select set_config('client_encoding', $1, false)", @@ -537,6 +548,6 @@ class AsyncConnection(BaseConnection): ) async def set_autocommit(self, value: bool) -> None: - """Async version of the `autocommit` setter.""" + """Async version of the `~Connection.autocommit` setter.""" async with self.lock: super()._set_autocommit(value) diff --git a/psycopg3/psycopg3/transaction.py b/psycopg3/psycopg3/transaction.py index 8e11ab04b..ce4b43e13 100644 --- a/psycopg3/psycopg3/transaction.py +++ b/psycopg3/psycopg3/transaction.py @@ -64,10 +64,12 @@ class BaseTransaction(Generic[ConnectionType]): @property def connection(self) -> ConnectionType: + """The connection the object is managing.""" return self._conn @property def savepoint_name(self) -> Optional[str]: + """The name of the savepoint; `None` if handling the main transaction.""" return self._savepoint_name def __repr__(self) -> str: @@ -135,6 +137,10 @@ class BaseTransaction(Generic[ConnectionType]): class Transaction(BaseTransaction["Connection"]): + """ + Returned by `Connection.transaction()` to handle a transaction block. + """ + def __enter__(self) -> "Transaction": with self._conn.lock: self._execute(self._enter_commands()) @@ -177,6 +183,10 @@ class Transaction(BaseTransaction["Connection"]): class AsyncTransaction(BaseTransaction["AsyncConnection"]): + """ + Returned by `AsyncConnection.transaction()` to handle a transaction block. + """ + async def __aenter__(self) -> "AsyncTransaction": async with self._conn.lock: await self._execute(self._enter_commands())