]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Added documentation for transaction management
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 18 Nov 2020 15:05:35 +0000 (15:05 +0000)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Wed, 18 Nov 2020 15:05:35 +0000 (15:05 +0000)
docs/_static/.keep [deleted file]
docs/_static/psycopg.css [new file with mode: 0644]
docs/connection.rst
docs/usage.rst
psycopg3/psycopg3/__init__.py
psycopg3/psycopg3/connection.py
psycopg3/psycopg3/transaction.py

diff --git a/docs/_static/.keep b/docs/_static/.keep
deleted file mode 100644 (file)
index e69de29..0000000
diff --git a/docs/_static/psycopg.css b/docs/_static/psycopg.css
new file mode 100644 (file)
index 0000000..3162978
--- /dev/null
@@ -0,0 +1,4 @@
+.hint {
+  background: #ffc;
+  border: 1px solid #dda;
+}
index d0317823f950ab1522825f0b40734324291ca779..03cb4441f162faa3dad2fb679e429cd715ab5f6f 100644 (file)
@@ -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)
index d4872a5c69ab95105c9cbf1b8c2c69a9d161a267..fc24d3187353eb803cc6e49901ec98e050383ff7 100644 (file)
@@ -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
index 6f6a907e279c8ee7671fe61ebafcf9605edcbc5a..13067887d7da389a092ec3b934cafd280a6036c8 100644 (file)
@@ -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
index 2dd262de432a02fce369559165917e09234d5fbb..e308b6b927b1592d4d8ba8a4790835c6d15a9cd7 100644 (file)
@@ -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)
index 8e11ab04bad8d4f5620335b5662764480c026b07..ce4b43e138f63ad0ddd0364d011fe270f09ee5a3 100644 (file)
@@ -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())