]> git.ipfire.org Git - thirdparty/psycopg.git/commitdiff
Add IsolationLevel and transaction control attributes on connection
authorDaniele Varrazzo <daniele.varrazzo@gmail.com>
Fri, 23 Jul 2021 13:01:34 +0000 (15:01 +0200)
committerDaniele Varrazzo <daniele.varrazzo@gmail.com>
Fri, 23 Jul 2021 14:39:39 +0000 (16:39 +0200)
Only add the documentation and the property: they don't work yet.

docs/api/connections.rst
docs/basic/transactions.rst
psycopg/psycopg/__init__.py
psycopg/psycopg/_enums.py
psycopg/psycopg/connection.py

index 714e40bf1d6c4f67b8d323868cd5936fb4aa84f9..24c49c4a521b68c4377a8ef854977fb8a4d1aa94 100644 (file)
@@ -165,9 +165,37 @@ The `!Connection` class
     .. autoattribute:: autocommit
 
         The property is writable for sync connections, read-only for async
-        ones: you should call ``await`` `~AsyncConnection.set_autocommit`\
+        ones: you should call ``await`` `~AsyncConnection.set_autocommit`
         :samp:`({value})` instead.
 
+    The following three properties control the characteristics of new
+    transactions. See :ref:`transaction-characteristics` for detils.
+
+    .. autoattribute:: isolation_level
+
+        `!None` means use the default set in the default_transaction_isolation__
+        configuration parameter of the server.
+
+        .. __: https://www.postgresql.org/docs/current/runtime-config-client.html
+               #GUC-DEFAULT-TRANSACTION-ISOLATION
+
+    .. autoattribute:: read_only
+
+        `!None` means use the default set in the default_transaction_read_only__
+        configuration parameter of the server.
+
+        .. __: https://www.postgresql.org/docs/current/runtime-config-client.html
+               #GUC-DEFAULT-TRANSACTION-READ-ONLY
+
+    .. autoattribute:: deferrable
+
+        `!None` means use the default set in the default_transaction_deferrable__
+        configuration parameter of the server.
+
+        .. __: https://www.postgresql.org/docs/current/runtime-config-client.html
+               #GUC-DEFAULT-TRANSACTION-DEFERRABLE
+
+
     .. rubric:: Checking and configuring the connection state
 
     .. autoattribute:: client_encoding
@@ -303,6 +331,9 @@ The `!AsyncConnection` class
     .. automethod:: notifies
     .. automethod:: set_client_encoding
     .. automethod:: set_autocommit
+    .. automethod:: set_isolation_level
+    .. automethod:: set_read_only
+    .. automethod:: set_deferrable
 
 
 Connection support objects
@@ -370,6 +401,16 @@ Connection support objects
 
 .. rubric:: Objects involved in :ref:`transactions`
 
+.. autoclass:: IsolationLevel
+    :members:
+
+    The value is usually used with the `Connection.isolation_level` property.
+
+    Check the PostgreSQL documentation for a description of the effects of the
+    different `levels of transaction isolation`__.
+
+    .. __: https://www.postgresql.org/docs/current/transaction-iso.html
+
 .. autoclass:: Transaction()
 
     .. autoattribute:: savepoint_name
index 74efb1d329c6053dba1817b4ed46aee98e03cef8..a7563f7c76b594d677a2daa81755c6b78fdf708f 100644 (file)
@@ -25,7 +25,7 @@ a `~rollback()` is called.
     *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 of error. You need to call
-    `!rollback()` if you want to keep on using the same connection.
+    `~Connection.rollback()` if you want to keep on using the same connection.
 
 
 .. _autocommit:
@@ -141,3 +141,43 @@ but not entirely committed yet.
 
     # If `Rollback` is raised, it would propagate only up to this block,
     # and the program would continue from here with no exception.
+
+
+.. _transaction-characteristics:
+
+Transaction characteristics
+---------------------------
+
+You can set `transaction parameters`__ for the transactions that Psycopg
+handles. They affect the transactions started implicitly by non-autocommit
+transactions and the ones started explicitly by `Connection.transaction()` for
+both autocommit and non-autocommit transactions. Leaving these parameters to
+`!None` will leave the behaviour to the server's default (which is controlled
+by server settings such as default_transaction_isolation__).
+
+.. __: https://www.postgresql.org/docs/current/sql-set-transaction.html
+.. __: https://www.postgresql.org/docs/current/runtime-config-client.html
+       #GUC-DEFAULT-TRANSACTION-ISOLATION
+
+In order to set these parameters you can use the connection attributes
+`~Connection.isolation_level`, `~Connection.read_only`,
+`~Connection.deferrable`. For async connections you must use the equivalent
+`~AsyncConnection.set_isolation_level()` method and similar. The parameters
+can only be changed if there isn't a transaction already active on the
+connection.
+
+.. warning::
+
+   Applications running at `~IsolationLevel.REPEATABLE_READ` or
+   `~IsolationLevel.SERIALIZABLE` isolation level are exposed to serialization
+   failures. `In certain concurrent update cases`__, PostgreSQL will raise an
+   exception looking like::
+
+        psycopg2.errors.SerializationFailure: could not serialize access
+        due to concurrent update
+
+   In this case the application must be prepared to repeat the operation that
+   caused the exception.
+
+   .. __: https://www.postgresql.org/docs/current/transaction-iso.html
+          #XACT-REPEATABLE-READ
index 5aa40c536161b5aa72170c625c7d582862b0e82f..5706794f49e802a253ed04c99f1fe41b8184754e 100644 (file)
@@ -10,6 +10,7 @@ from . import pq
 from . import types
 from . import postgres
 from .copy import Copy, AsyncCopy
+from ._enums import IsolationLevel
 from .cursor import AnyCursor, AsyncCursor, Cursor
 from .errors import Warning, Error, InterfaceError, DatabaseError
 from .errors import DataError, OperationalError, IntegrityError
@@ -61,6 +62,7 @@ __all__ = [
     "Connection",
     "Copy",
     "Cursor",
+    "IsolationLevel",
     "Notify",
     "Rollback",
     "ServerCursor",
index 7ccc1d1e693df0b36f646091e375945de80653a8..070fd1da50ecbfa701a2f82e72b02f284f2b9e41 100644 (file)
@@ -38,6 +38,23 @@ class PyFormat(str, Enum):
         return _py2pg[fmt]
 
 
+class IsolationLevel(Enum):
+    """
+    Enum representing the isolation level for a transaction.
+    """
+
+    __module__ = "psycopg"
+
+    READ_UNCOMMITTED = 1
+    """:sql:`READ UNCOMMITTED` isolation level."""
+    READ_COMMITTED = 2
+    """:sql:`READ COMMITTED` isolation level."""
+    REPEATABLE_READ = 3
+    """:sql:`REPEATABLE READ` isolation level."""
+    SERIALIZABLE = 4
+    """:sql:`SERIALIZABLE` isolation level."""
+
+
 _py2pg = {
     PyFormat.TEXT: pq.Format.TEXT,
     PyFormat.BINARY: pq.Format.BINARY,
index 2a8481eba4fc9b35b89d3745152f5747f3e8e3e1..5c6c0ff67ec98da9cabe3e72f981ed54b2ddb3ba 100644 (file)
@@ -26,6 +26,7 @@ from .pq import ConnStatus, ExecStatus, TransactionStatus, Format
 from .abc import ConnectionType, Params, PQGen, PQGenConn, Query, RV
 from .sql import Composable
 from .rows import Row, RowFactory, tuple_row, TupleRow
+from ._enums import IsolationLevel
 from .compat import asynccontextmanager
 from .cursor import Cursor, AsyncCursor
 from ._cmodule import _psycopg
@@ -129,6 +130,10 @@ class BaseConnection(Generic[Row]):
         # Time after which the connection should be closed
         self._expire_at: float
 
+        self._isolation_level: Optional[IsolationLevel] = None
+        self._read_only: Optional[bool] = None
+        self._deferrable: Optional[bool] = None
+
     def __del__(self) -> None:
         # If fails on connection we might not have this attribute yet
         if not hasattr(self, "pgconn"):
@@ -178,24 +183,80 @@ class BaseConnection(Generic[Row]):
         self._set_autocommit(value)
 
     def _set_autocommit(self, value: bool) -> None:
-        # Base implementation, not thread safe
-        # subclasses must call it holding a lock
+        # Base implementation, not thread safe.
+        # Subclasses must call it holding a lock
+        self._check_intrans("autocommit")
+        self._autocommit = value
+
+    @property
+    def isolation_level(self) -> Optional[IsolationLevel]:
+        """
+        The isolation level of the new transactions started on the connection.
+        """
+        return self._isolation_level
+
+    @isolation_level.setter
+    def isolation_level(self, value: Optional[IsolationLevel]) -> None:
+        self._set_isolation_level(value)
+
+    def _set_isolation_level(self, value: Optional[IsolationLevel]) -> None:
+        # Base implementation, not thread safe.
+        # Subclasses must call it holding a lock
+        self._check_intrans("isolation_level")
+        self._isolation_level = (
+            IsolationLevel(value) if value is not None else None
+        )
+
+    @property
+    def read_only(self) -> Optional[bool]:
+        """
+        The read-only state of the new transactions started on the connection.
+        """
+        return self._read_only
+
+    @read_only.setter
+    def read_only(self, value: Optional[bool]) -> None:
+        self._set_read_only(value)
+
+    def _set_read_only(self, value: Optional[bool]) -> None:
+        # Base implementation, not thread safe.
+        # Subclasses must call it holding a lock
+        self._check_intrans("read_only")
+        self._read_only = value
+
+    @property
+    def deferrable(self) -> Optional[bool]:
+        """
+        The deferrable state of the new transactions started on the connection.
+        """
+        return self._deferrable
+
+    @deferrable.setter
+    def deferrable(self, value: Optional[bool]) -> None:
+        self._set_deferrable(value)
+
+    def _set_deferrable(self, value: Optional[bool]) -> None:
+        # Base implementation, not thread safe.
+        # Subclasses must call it holding a lock
+        self._check_intrans("deferrable")
+        self._deferrable = value
+
+    def _check_intrans(self, attribute: str) -> None:
+        # Raise an exception if we are in a transaction
         status = self.pgconn.transaction_status
         if status != TransactionStatus.IDLE:
             if self._savepoints:
                 raise e.ProgrammingError(
-                    "couldn't change autocommit state: "
+                    f"can't change {attribute!r} now: "
                     "connection.transaction() context in progress"
                 )
             else:
                 raise e.ProgrammingError(
-                    "couldn't change autocommit state: "
+                    f"can't change {attribute!r} now: "
                     "connection in transaction status "
                     f"{TransactionStatus(status).name}"
                 )
 
-        self._autocommit = value
-
     @property
     def client_encoding(self) -> str:
         """The Python codec name of the connection's client encoding."""
@@ -671,6 +732,18 @@ class Connection(BaseConnection[Row]):
         with self.lock:
             super()._set_autocommit(value)
 
+    def _set_isolation_level(self, value: Optional[IsolationLevel]) -> None:
+        with self.lock:
+            super()._set_isolation_level(value)
+
+    def _set_read_only(self, value: Optional[bool]) -> None:
+        with self.lock:
+            super()._set_read_only(value)
+
+    def _set_deferrable(self, value: Optional[bool]) -> None:
+        with self.lock:
+            super()._set_deferrable(value)
+
     def _set_client_encoding(self, name: str) -> None:
         with self.lock:
             self.wait(self._set_client_encoding_gen(name))
@@ -888,26 +961,50 @@ class AsyncConnection(BaseConnection[Row]):
     ) -> RV:
         return await waiting.wait_conn_async(gen, timeout)
 
+    def _set_autocommit(self, value: bool) -> None:
+        self._no_set_async("autocommit")
+
+    async def set_autocommit(self, value: bool) -> None:
+        """Async version of the `~Connection.autocommit` setter."""
+        async with self.lock:
+            super()._set_autocommit(value)
+
+    def _set_isolation_level(self, value: Optional[IsolationLevel]) -> None:
+        self._no_set_async("isolation_level")
+
+    async def set_isolation_level(
+        self, value: Optional[IsolationLevel]
+    ) -> None:
+        """Async version of the `~Connection.isolation_level` setter."""
+        async with self.lock:
+            super()._set_isolation_level(value)
+
+    def _set_read_only(self, value: Optional[bool]) -> None:
+        self._no_set_async("read_only")
+
+    async def set_read_only(self, value: Optional[bool]) -> None:
+        """Async version of the `~Connection.read_only` setter."""
+        async with self.lock:
+            super()._set_read_only(value)
+
+    def _set_deferrable(self, value: Optional[bool]) -> None:
+        self._no_set_async("deferrable")
+
+    async def set_deferrable(self, value: Optional[bool]) -> None:
+        """Async version of the `~Connection.deferrable` setter."""
+        async with self.lock:
+            super()._set_deferrable(value)
+
     def _set_client_encoding(self, name: str) -> None:
-        raise AttributeError(
-            "'client_encoding' is read-only on async connections:"
-            " please use await .set_client_encoding() instead."
-        )
+        self._no_set_async("client_encoding")
 
     async def set_client_encoding(self, name: str) -> None:
         """Async version of the `~Connection.client_encoding` setter."""
         async with self.lock:
             await self.wait(self._set_client_encoding_gen(name))
 
-    def _set_autocommit(self, value: bool) -> None:
+    def _no_set_async(self, attribute: str) -> None:
         raise AttributeError(
-            "autocommit is read-only on async connections:"
-            " please use await connection.set_autocommit() instead."
-            " Note that you can pass an 'autocommit' value to 'connect()'"
-            " if it doesn't need to change during the connection's lifetime."
+            f"'the {attribute!r} property is read-only on async connections:"
+            f" please use 'await .set_{attribute}()' instead."
         )
-
-    async def set_autocommit(self, value: bool) -> None:
-        """Async version of the `~Connection.autocommit` setter."""
-        async with self.lock:
-            super()._set_autocommit(value)