From: Daniele Varrazzo Date: Fri, 23 Jul 2021 14:28:58 +0000 (+0200) Subject: Implement the transaction control attributes X-Git-Tag: 3.0.dev2~45^2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=3bf555db83cd8ddcb8e0bb87bf4c02f63a53d780;p=thirdparty%2Fpsycopg.git Implement the transaction control attributes --- diff --git a/psycopg/psycopg/_enums.py b/psycopg/psycopg/_enums.py index 070fd1da5..5f553864f 100644 --- a/psycopg/psycopg/_enums.py +++ b/psycopg/psycopg/_enums.py @@ -7,7 +7,7 @@ libpq-defined enums. # Copyright (C) 2020-2021 The Psycopg Team -from enum import Enum +from enum import Enum, IntEnum from . import pq @@ -38,7 +38,7 @@ class PyFormat(str, Enum): return _py2pg[fmt] -class IsolationLevel(Enum): +class IsolationLevel(IntEnum): """ Enum representing the isolation level for a transaction. """ diff --git a/psycopg/psycopg/connection.py b/psycopg/psycopg/connection.py index 5c6c0ff67..0cdd55846 100644 --- a/psycopg/psycopg/connection.py +++ b/psycopg/psycopg/connection.py @@ -469,7 +469,25 @@ class BaseConnection(Generic[Row]): if self.pgconn.transaction_status != TransactionStatus.IDLE: return - yield from self._exec_command(b"begin") + yield from self._exec_command(self._get_tx_start_command()) + + def _get_tx_start_command(self) -> bytes: + parts = [b"begin"] + + if self.isolation_level is not None: + val = IsolationLevel(self.isolation_level) + parts.append(b"isolation level") + parts.append(val.name.lower().replace("_", " ").encode("utf8")) + + if self.read_only is not None: + parts.append(b"read only" if self.read_only else b"read write") + + if self.deferrable is not None: + parts.append( + b"deferrable" if self.deferrable else b"not deferrable" + ) + + return b" ".join(parts) def _commit_gen(self) -> PQGen[None]: """Generator implementing `Connection.commit()`.""" diff --git a/psycopg/psycopg/transaction.py b/psycopg/psycopg/transaction.py index 2804e1e6f..85a5c3a17 100644 --- a/psycopg/psycopg/transaction.py +++ b/psycopg/psycopg/transaction.py @@ -99,7 +99,7 @@ class BaseTransaction(Generic[ConnectionType]): commands = [] if self._outer_transaction: assert not self._conn._savepoints, self._conn._savepoints - commands.append(b"begin") + commands.append(self._conn._get_tx_start_command()) if self._savepoint_name: commands.append( diff --git a/tests/test_connection.py b/tests/test_connection.py index 3bf87b3dc..c7c78cd45 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -566,3 +566,107 @@ def test_server_cursor_factory(conn): conn.server_cursor_factory = MyServerCursor with conn.cursor(name="n") as cur: assert isinstance(cur, MyServerCursor) + + +tx_params = { + "isolation_level": { + "guc": "isolation", + "values": list(psycopg.IsolationLevel), + "set_method": "set_isolation_level", + }, + "read_only": { + "guc": "read_only", + "values": [True, False], + "set_method": "set_read_only", + }, + "deferrable": { + "guc": "deferrable", + "values": [True, False], + "set_method": "set_deferrable", + }, +} + +# Map Python values to Postgres values for the tx_params possible values +tx_values_map = { + v.name.lower().replace("_", " "): v.value for v in psycopg.IsolationLevel +} +tx_values_map["on"] = True +tx_values_map["off"] = False + + +@pytest.mark.parametrize("attr", list(tx_params)) +def test_transaction_param_default(conn, attr): + assert getattr(conn, attr) is None + guc = tx_params[attr]["guc"] + current, default = conn.execute( + "select current_setting(%s), current_setting(%s)", + [f"transaction_{guc}", f"default_transaction_{guc}"], + ).fetchone() + assert current == default + + +@pytest.mark.parametrize("autocommit", [True, False]) +@pytest.mark.parametrize("attr", list(tx_params)) +def test_set_transaction_param_implicit(conn, attr, autocommit): + guc = tx_params[attr]["guc"] + conn.autocommit = autocommit + for value in tx_params[attr]["values"]: + setattr(conn, attr, value) + pgval, default = conn.execute( + "select current_setting(%s), current_setting(%s)", + [f"transaction_{guc}", f"default_transaction_{guc}"], + ).fetchone() + if autocommit: + assert pgval == default + else: + assert tx_values_map[pgval] == value + conn.rollback() + + +@pytest.mark.parametrize("autocommit", [True, False]) +@pytest.mark.parametrize("attr", list(tx_params)) +def test_set_transaction_param_block(conn, attr, autocommit): + guc = tx_params[attr]["guc"] + conn.autocommit = autocommit + for value in tx_params[attr]["values"]: + setattr(conn, attr, value) + with conn.transaction(): + pgval = conn.execute( + "select current_setting(%s)", [f"transaction_{guc}"] + ).fetchone()[0] + assert tx_values_map[pgval] == value + + +@pytest.mark.parametrize("attr", list(tx_params)) +def test_set_transaction_param_not_intrans_implicit(conn, attr): + conn.execute("select 1") + with pytest.raises(psycopg.ProgrammingError): + setattr(conn, attr, tx_params[attr]["values"][0]) + + +@pytest.mark.parametrize("attr", list(tx_params)) +def test_set_transaction_param_not_intrans_block(conn, attr): + with conn.transaction(): + with pytest.raises(psycopg.ProgrammingError): + setattr(conn, attr, tx_params[attr]["values"][0]) + + +@pytest.mark.parametrize("attr", list(tx_params)) +def test_set_transaction_param_not_intrans_external(conn, attr): + conn.autocommit = True + conn.execute("begin") + with pytest.raises(psycopg.ProgrammingError): + setattr(conn, attr, tx_params[attr]["values"][0]) + + +def test_set_transaction_param_all(conn): + for attr in tx_params: + value = tx_params[attr]["values"][0] + setattr(conn, attr, value) + + for attr in tx_params: + guc = tx_params[attr]["guc"] + pgval = conn.execute( + "select current_setting(%s)", [f"transaction_{guc}"] + ).fetchone()[0] + assert tx_values_map[pgval] == value diff --git a/tests/test_connection_async.py b/tests/test_connection_async.py index eb9578783..0e13dff0d 100644 --- a/tests/test_connection_async.py +++ b/tests/test_connection_async.py @@ -14,6 +14,7 @@ from psycopg.conninfo import conninfo_to_dict from .utils import gc_collect from .test_cursor import my_row_factory +from .test_connection import tx_params, tx_values_map pytestmark = pytest.mark.asyncio @@ -584,3 +585,94 @@ async def test_server_cursor_factory(aconn): aconn.server_cursor_factory = MyServerCursor async with aconn.cursor(name="n") as cur: assert isinstance(cur, MyServerCursor) + + +@pytest.mark.parametrize("attr", list(tx_params)) +async def test_transaction_param_default(aconn, attr): + assert getattr(aconn, attr) is None + guc = tx_params[attr]["guc"] + cur = await aconn.execute( + "select current_setting(%s), current_setting(%s)", + [f"transaction_{guc}", f"default_transaction_{guc}"], + ) + current, default = await cur.fetchone() + assert current == default + + +@pytest.mark.parametrize("attr", list(tx_params)) +async def test_transaction_param_readonly_property(aconn, attr): + with pytest.raises(AttributeError): + setattr(aconn, attr, None) + + +@pytest.mark.parametrize("autocommit", [True, False]) +@pytest.mark.parametrize("attr", list(tx_params)) +async def test_set_transaction_param_implicit(aconn, attr, autocommit): + guc = tx_params[attr]["guc"] + await aconn.set_autocommit(autocommit) + for value in tx_params[attr]["values"]: + await getattr(aconn, tx_params[attr]["set_method"])(value) + cur = await aconn.execute( + "select current_setting(%s), current_setting(%s)", + [f"transaction_{guc}", f"default_transaction_{guc}"], + ) + pgval, default = await cur.fetchone() + if autocommit: + assert pgval == default + else: + assert tx_values_map[pgval] == value + await aconn.rollback() + + +@pytest.mark.parametrize("autocommit", [True, False]) +@pytest.mark.parametrize("attr", list(tx_params)) +async def test_set_transaction_param_block(aconn, attr, autocommit): + guc = tx_params[attr]["guc"] + await aconn.set_autocommit(autocommit) + for value in tx_params[attr]["values"]: + await getattr(aconn, tx_params[attr]["set_method"])(value) + async with aconn.transaction(): + cur = await aconn.execute( + "select current_setting(%s)", [f"transaction_{guc}"] + ) + pgval = (await cur.fetchone())[0] + assert tx_values_map[pgval] == value + + +@pytest.mark.parametrize("attr", list(tx_params)) +async def test_set_transaction_param_not_intrans_implicit(aconn, attr): + await aconn.execute("select 1") + value = tx_params[attr]["values"][0] + with pytest.raises(psycopg.ProgrammingError): + await getattr(aconn, tx_params[attr]["set_method"])(value) + + +@pytest.mark.parametrize("attr", list(tx_params)) +async def test_set_transaction_param_not_intrans_block(aconn, attr): + value = tx_params[attr]["values"][0] + async with aconn.transaction(): + with pytest.raises(psycopg.ProgrammingError): + await getattr(aconn, tx_params[attr]["set_method"])(value) + + +@pytest.mark.parametrize("attr", list(tx_params)) +async def test_set_transaction_param_not_intrans_external(aconn, attr): + value = tx_params[attr]["values"][0] + await aconn.set_autocommit(True) + await aconn.execute("begin") + with pytest.raises(psycopg.ProgrammingError): + await getattr(aconn, tx_params[attr]["set_method"])(value) + + +async def test_set_transaction_param_all(aconn): + for attr in tx_params: + value = tx_params[attr]["values"][0] + await getattr(aconn, tx_params[attr]["set_method"])(value) + + for attr in tx_params: + guc = tx_params[attr]["guc"] + cur = await aconn.execute( + "select current_setting(%s)", [f"transaction_{guc}"] + ) + pgval = (await cur.fetchone())[0] + assert tx_values_map[pgval] == value