From: Daniele Varrazzo Date: Fri, 23 Jul 2021 13:01:34 +0000 (+0200) Subject: Add IsolationLevel and transaction control attributes on connection X-Git-Tag: 3.0.dev2~45^2~1 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=e76c0cc64f08a9bff6416377260531a4a587a002;p=thirdparty%2Fpsycopg.git Add IsolationLevel and transaction control attributes on connection Only add the documentation and the property: they don't work yet. --- diff --git a/docs/api/connections.rst b/docs/api/connections.rst index 714e40bf1..24c49c4a5 100644 --- a/docs/api/connections.rst +++ b/docs/api/connections.rst @@ -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 diff --git a/docs/basic/transactions.rst b/docs/basic/transactions.rst index 74efb1d32..a7563f7c7 100644 --- a/docs/basic/transactions.rst +++ b/docs/basic/transactions.rst @@ -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 diff --git a/psycopg/psycopg/__init__.py b/psycopg/psycopg/__init__.py index 5aa40c536..5706794f4 100644 --- a/psycopg/psycopg/__init__.py +++ b/psycopg/psycopg/__init__.py @@ -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", diff --git a/psycopg/psycopg/_enums.py b/psycopg/psycopg/_enums.py index 7ccc1d1e6..070fd1da5 100644 --- a/psycopg/psycopg/_enums.py +++ b/psycopg/psycopg/_enums.py @@ -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, diff --git a/psycopg/psycopg/connection.py b/psycopg/psycopg/connection.py index 2a8481eba..5c6c0ff67 100644 --- a/psycopg/psycopg/connection.py +++ b/psycopg/psycopg/connection.py @@ -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)