Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions asyncpg/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ def get_settings(self):
"""
return self._protocol.get_settings()

def transaction(self, *, isolation='read_committed', readonly=False,
def transaction(self, *, isolation=None, readonly=False,
deferrable=False):
"""Create a :class:`~transaction.Transaction` object.

Expand All @@ -237,7 +237,9 @@ def transaction(self, *, isolation='read_committed', readonly=False,

:param isolation: Transaction isolation mode, can be one of:
`'serializable'`, `'repeatable_read'`,
`'read_committed'`.
`'read_committed'`. If not specified, the behavior
is up to the server and session, which is usually
``read_committed``.

:param readonly: Specifies whether or not this transaction is
read-only.
Expand Down
27 changes: 19 additions & 8 deletions asyncpg/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@


import enum
import warnings

from . import connresource
from . import exceptions as apg_errors
Expand Down Expand Up @@ -36,12 +37,12 @@ class Transaction(connresource.ConnectionResource):
def __init__(self, connection, isolation, readonly, deferrable):
super().__init__(connection)

if isolation not in ISOLATION_LEVELS:
if isolation and isolation not in ISOLATION_LEVELS:
raise ValueError(
'isolation is expected to be either of {}, '
'got {!r}'.format(ISOLATION_LEVELS, isolation))

if isolation != 'serializable':
if isolation and isolation != 'serializable':
if readonly:
raise ValueError(
'"readonly" is only supported for '
Expand Down Expand Up @@ -111,19 +112,29 @@ async def start(self):
else:
# Nested transaction block
top_xact = con._top_xact
if self._isolation != top_xact._isolation:
raise apg_errors.InterfaceError(
'nested transaction has a different isolation level: '
'current {!r} != outer {!r}'.format(
self._isolation, top_xact._isolation))
if top_xact._isolation is None:
if self._isolation:
w = apg_errors.InterfaceWarning(
'nested transaction may have a different isolation '
'level: current {!r}, outer unknown'.format(
self._isolation))
warnings.warn(w)
elif self._isolation:
if self._isolation != top_xact._isolation:
raise apg_errors.InterfaceError(
'nested transaction has a different isolation level: '
'current {!r} != outer {!r}'.format(
self._isolation, top_xact._isolation))
self._nested = True

if self._nested:
self._id = con._get_unique_id('savepoint')
query = 'SAVEPOINT {};'.format(self._id)
else:
if self._isolation == 'read_committed':
if self._isolation is None:
query = 'BEGIN;'
elif self._isolation == 'read_committed':
query = 'BEGIN ISOLATION LEVEL READ COMMITTED;'
elif self._isolation == 'repeatable_read':
query = 'BEGIN ISOLATION LEVEL REPEATABLE READ;'
else:
Expand Down
65 changes: 65 additions & 0 deletions tests/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,68 @@ async def test_transaction_within_manual_transaction(self):

self.assertIsNone(self.con._top_xact)
self.assertFalse(self.con.is_in_transaction())

async def test_isolation_level(self):
await self.con.reset()
default_isolation = await self.con.fetchval(
'SHOW default_transaction_isolation'
)
isolation_levels = {
None: default_isolation,
'read_committed': 'read committed',
'repeatable_read': 'repeatable read',
'serializable': 'serializable',
}
set_sql = 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
get_sql = 'SHOW TRANSACTION ISOLATION LEVEL'
for tx_level in isolation_levels:
for conn_level in isolation_levels:
with self.subTest(conn=conn_level, tx=tx_level):
if conn_level:
await self.con.execute(
set_sql + isolation_levels[conn_level]
)
level = await self.con.fetchval(get_sql)
self.assertEqual(level, isolation_levels[conn_level])
async with self.con.transaction(isolation=tx_level):
level = await self.con.fetchval(get_sql)
self.assertEqual(
level,
isolation_levels[tx_level or conn_level],
)
await self.con.reset()

async def test_nested_isolation_level(self):
isolation_levels = {
None,
'read_committed',
'repeatable_read',
'serializable',
}
for inner in isolation_levels:
for outer in isolation_levels:
with self.subTest(outer=outer, inner=inner):
async with self.con.transaction(isolation=outer):
if outer and inner and outer != inner:
with self.assertRaisesRegex(
asyncpg.InterfaceError,
'current {!r} != outer {!r}'.format(
inner, outer
)
):
async with self.con.transaction(
isolation=inner,
):
pass
elif not outer and inner:
with self.assertWarnsRegex(
asyncpg.InterfaceWarning,
'current {!r}, outer unknown'.format(inner),
):
async with self.con.transaction(
isolation=inner,
):
pass
else:
async with self.con.transaction(isolation=inner):
pass