Skip to content

Commit 4a627d5

Browse files
authored
Fix wrong default transaction isolation level (#622)
* Fix wrong default transaction isolation level This fixes the issue when the default_transaction_isolation is not "read committed", `transaction(isolation='read_committed')` won't start a transaction in "read committed" isolation level.
1 parent 2bac166 commit 4a627d5

File tree

3 files changed

+92
-11
lines changed

3 files changed

+92
-11
lines changed

asyncpg/connection.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ def get_settings(self):
228228
"""
229229
return self._protocol.get_settings()
230230

231-
def transaction(self, *, isolation='read_committed', readonly=False,
231+
def transaction(self, *, isolation=None, readonly=False,
232232
deferrable=False):
233233
"""Create a :class:`~transaction.Transaction` object.
234234
@@ -237,7 +237,9 @@ def transaction(self, *, isolation='read_committed', readonly=False,
237237
238238
:param isolation: Transaction isolation mode, can be one of:
239239
`'serializable'`, `'repeatable_read'`,
240-
`'read_committed'`.
240+
`'read_committed'`. If not specified, the behavior
241+
is up to the server and session, which is usually
242+
``read_committed``.
241243
242244
:param readonly: Specifies whether or not this transaction is
243245
read-only.

asyncpg/transaction.py

+21-9
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ class TransactionState(enum.Enum):
2020

2121

2222
ISOLATION_LEVELS = {'read_committed', 'serializable', 'repeatable_read'}
23+
ISOLATION_LEVELS_BY_VALUE = {
24+
'read committed': 'read_committed',
25+
'serializable': 'serializable',
26+
'repeatable read': 'repeatable_read',
27+
}
2328

2429

2530
class Transaction(connresource.ConnectionResource):
@@ -36,12 +41,12 @@ class Transaction(connresource.ConnectionResource):
3641
def __init__(self, connection, isolation, readonly, deferrable):
3742
super().__init__(connection)
3843

39-
if isolation not in ISOLATION_LEVELS:
44+
if isolation and isolation not in ISOLATION_LEVELS:
4045
raise ValueError(
4146
'isolation is expected to be either of {}, '
4247
'got {!r}'.format(ISOLATION_LEVELS, isolation))
4348

44-
if isolation != 'serializable':
49+
if isolation and isolation != 'serializable':
4550
if readonly:
4651
raise ValueError(
4752
'"readonly" is only supported for '
@@ -110,20 +115,27 @@ async def start(self):
110115
con._top_xact = self
111116
else:
112117
# Nested transaction block
113-
top_xact = con._top_xact
114-
if self._isolation != top_xact._isolation:
115-
raise apg_errors.InterfaceError(
116-
'nested transaction has a different isolation level: '
117-
'current {!r} != outer {!r}'.format(
118-
self._isolation, top_xact._isolation))
118+
if self._isolation:
119+
top_xact_isolation = con._top_xact._isolation
120+
if top_xact_isolation is None:
121+
top_xact_isolation = ISOLATION_LEVELS_BY_VALUE[
122+
await self._connection.fetchval(
123+
'SHOW transaction_isolation;')]
124+
if self._isolation != top_xact_isolation:
125+
raise apg_errors.InterfaceError(
126+
'nested transaction has a different isolation level: '
127+
'current {!r} != outer {!r}'.format(
128+
self._isolation, top_xact_isolation))
119129
self._nested = True
120130

121131
if self._nested:
122132
self._id = con._get_unique_id('savepoint')
123133
query = 'SAVEPOINT {};'.format(self._id)
124134
else:
125-
if self._isolation == 'read_committed':
135+
if self._isolation is None:
126136
query = 'BEGIN;'
137+
elif self._isolation == 'read_committed':
138+
query = 'BEGIN ISOLATION LEVEL READ COMMITTED;'
127139
elif self._isolation == 'repeatable_read':
128140
query = 'BEGIN ISOLATION LEVEL REPEATABLE READ;'
129141
else:

tests/test_transaction.py

+67
Original file line numberDiff line numberDiff line change
@@ -179,3 +179,70 @@ async def test_transaction_within_manual_transaction(self):
179179

180180
self.assertIsNone(self.con._top_xact)
181181
self.assertFalse(self.con.is_in_transaction())
182+
183+
async def test_isolation_level(self):
184+
await self.con.reset()
185+
default_isolation = await self.con.fetchval(
186+
'SHOW default_transaction_isolation'
187+
)
188+
isolation_levels = {
189+
None: default_isolation,
190+
'read_committed': 'read committed',
191+
'repeatable_read': 'repeatable read',
192+
'serializable': 'serializable',
193+
}
194+
set_sql = 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
195+
get_sql = 'SHOW TRANSACTION ISOLATION LEVEL'
196+
for tx_level in isolation_levels:
197+
for conn_level in isolation_levels:
198+
with self.subTest(conn=conn_level, tx=tx_level):
199+
if conn_level:
200+
await self.con.execute(
201+
set_sql + isolation_levels[conn_level]
202+
)
203+
level = await self.con.fetchval(get_sql)
204+
self.assertEqual(level, isolation_levels[conn_level])
205+
async with self.con.transaction(isolation=tx_level):
206+
level = await self.con.fetchval(get_sql)
207+
self.assertEqual(
208+
level,
209+
isolation_levels[tx_level or conn_level],
210+
)
211+
await self.con.reset()
212+
213+
async def test_nested_isolation_level(self):
214+
set_sql = 'SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL '
215+
isolation_levels = {
216+
'read_committed': 'read committed',
217+
'repeatable_read': 'repeatable read',
218+
'serializable': 'serializable',
219+
}
220+
for inner in [None] + list(isolation_levels):
221+
for outer, outer_sql_level in isolation_levels.items():
222+
for implicit in [False, True]:
223+
with self.subTest(
224+
implicit=implicit, outer=outer, inner=inner,
225+
):
226+
if implicit:
227+
await self.con.execute(set_sql + outer_sql_level)
228+
outer_level = None
229+
else:
230+
outer_level = outer
231+
232+
async with self.con.transaction(isolation=outer_level):
233+
if inner and outer != inner:
234+
with self.assertRaisesRegex(
235+
asyncpg.InterfaceError,
236+
'current {!r} != outer {!r}'.format(
237+
inner, outer
238+
)
239+
):
240+
async with self.con.transaction(
241+
isolation=inner,
242+
):
243+
pass
244+
else:
245+
async with self.con.transaction(
246+
isolation=inner,
247+
):
248+
pass

0 commit comments

Comments
 (0)