Skip to content

Commit

Permalink
Separated autocommit and isolation level handling for PostgreSQL.
Browse files Browse the repository at this point in the history
Autocommit cannot be manipulated independently from an open connection.
This commit introduces a minor change in behavior: entering transaction
management forces opening a databasse connection. This shouldn't be
backwards incompatible in any practical use case.
  • Loading branch information
aaugustin committed Mar 11, 2013
1 parent f515619 commit 8717b06
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 41 deletions.
78 changes: 41 additions & 37 deletions django/db/backends/postgresql_psycopg2/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,21 +77,21 @@ class DatabaseWrapper(BaseDatabaseWrapper):
def __init__(self, *args, **kwargs):
super(DatabaseWrapper, self).__init__(*args, **kwargs)

opts = self.settings_dict["OPTIONS"]
RC = psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED
self.isolation_level = opts.get('isolation_level', RC)

self.features = DatabaseFeatures(self)
autocommit = self.settings_dict["OPTIONS"].get('autocommit', False)
self.features.uses_autocommit = autocommit
if autocommit:
level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
else:
level = self.settings_dict["OPTIONS"].get('isolation_level',
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED)
self._set_isolation_level(level)
self.ops = DatabaseOperations(self)
self.client = DatabaseClient(self)
self.creation = DatabaseCreation(self)
self.introspection = DatabaseIntrospection(self)
self.validation = BaseDatabaseValidation(self)

autocommit = opts.get('autocommit', False)
self.features.uses_autocommit = autocommit
self.features.uses_savepoints = not autocommit

def get_connection_params(self):
settings_dict = self.settings_dict
if not settings_dict['NAME']:
Expand Down Expand Up @@ -135,11 +135,12 @@ def init_connection_state(self):

if conn_tz != tz:
# Set the time zone in autocommit mode (see #17062)
self.connection.set_isolation_level(
psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
self.set_autocommit(True)
self.connection.cursor().execute(
self.ops.set_time_zone_sql(), [tz])
self.connection.set_isolation_level(self.isolation_level)
if self.features.uses_autocommit:
self.set_autocommit(True)

def create_cursor(self):
cursor = self.connection.cursor()
Expand Down Expand Up @@ -172,42 +173,40 @@ def _enter_transaction_management(self, managed):
Switch the isolation level when needing transaction support, so that
the same transaction is visible across all the queries.
"""
if self.features.uses_autocommit and managed and not self.isolation_level:
level = self.settings_dict["OPTIONS"].get('isolation_level',
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED)
self._set_isolation_level(level)
if self.connection is None: # Force creating a connection.
self.cursor().close()
if self.features.uses_autocommit and managed and self.autocommit:
self.set_autocommit(False)
self.features.uses_savepoints = True

def _leave_transaction_management(self, managed):
"""
If the normal operating mode is "autocommit", switch back to that when
leaving transaction management.
"""
if self.features.uses_autocommit and not managed and self.isolation_level:
self._set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)

def _set_isolation_level(self, level):
"""
Do all the related feature configurations for changing isolation
levels. This doesn't touch the uses_autocommit feature, since that
controls the movement *between* isolation levels.
"""
assert level in range(5)
try:
if self.connection is not None:
self.connection.set_isolation_level(level)
if level == psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT:
self.set_clean()
finally:
self.isolation_level = level
self.features.uses_savepoints = bool(level)
if self.connection is None: # Force creating a connection.
self.cursor().close()
if self.features.uses_autocommit and not managed and not self.autocommit:
self.rollback() # Must terminate transaction first.
self.set_autocommit(True)
self.features.uses_savepoints = False

def _set_isolation_level(self, isolation_level):
assert isolation_level in range(1, 5) # Use set_autocommit for level = 0
if self.psycopg2_version >= (2, 4, 2):
self.connection.set_session(isolation_level=isolation_level)
else:
self.connection.set_isolation_level(isolation_level)

def _set_autocommit(self, autocommit):
if autocommit:
level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
if self.psycopg2_version >= (2, 4, 2):
self.connection.autocommit = autocommit
else:
level = self.settings_dict["OPTIONS"].get('isolation_level',
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED)
self._set_isolation_level(level)
if autocommit:
level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
else:
level = self.isolation_level
self.connection.set_isolation_level(level)

def set_dirty(self):
if ((self.transaction_state and self.transaction_state[-1]) or
Expand All @@ -231,6 +230,11 @@ def is_usable(self):
else:
return True

@cached_property
def psycopg2_version(self):
version = psycopg2.__version__.split(' ', 1)[0]
return tuple(int(v) for v in version.split('.'))

@cached_property
def pg_version(self):
with self.temporary_connection():
Expand Down
17 changes: 13 additions & 4 deletions tests/transactions_regress/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,31 +274,39 @@ def tearDown(self):
connections[DEFAULT_DB_ALIAS] = self._old_backend

def test_initial_autocommit_state(self):
# Autocommit is activated when the connection is created.
connection.cursor().close()

self.assertTrue(connection.features.uses_autocommit)
self.assertEqual(connection.isolation_level, self._autocommit)
self.assertTrue(connection.autocommit)

def test_transaction_management(self):
transaction.enter_transaction_management()
self.assertFalse(connection.autocommit)
self.assertEqual(connection.isolation_level, self._serializable)

transaction.leave_transaction_management()
self.assertEqual(connection.isolation_level, self._autocommit)
self.assertTrue(connection.autocommit)

def test_transaction_stacking(self):
transaction.enter_transaction_management()
self.assertFalse(connection.autocommit)
self.assertEqual(connection.isolation_level, self._serializable)

transaction.enter_transaction_management()
self.assertFalse(connection.autocommit)
self.assertEqual(connection.isolation_level, self._serializable)

transaction.leave_transaction_management()
self.assertFalse(connection.autocommit)
self.assertEqual(connection.isolation_level, self._serializable)

transaction.leave_transaction_management()
self.assertEqual(connection.isolation_level, self._autocommit)
self.assertTrue(connection.autocommit)

def test_enter_autocommit(self):
transaction.enter_transaction_management()
self.assertFalse(connection.autocommit)
self.assertEqual(connection.isolation_level, self._serializable)
list(Mod.objects.all())
self.assertTrue(transaction.is_dirty())
Expand All @@ -311,9 +319,10 @@ def test_enter_autocommit(self):
list(Mod.objects.all())
self.assertFalse(transaction.is_dirty())
transaction.leave_transaction_management()
self.assertFalse(connection.autocommit)
self.assertEqual(connection.isolation_level, self._serializable)
transaction.leave_transaction_management()
self.assertEqual(connection.isolation_level, self._autocommit)
self.assertTrue(connection.autocommit)


class TestManyToManyAddTransaction(TransactionTestCase):
Expand Down

0 comments on commit 8717b06

Please sign in to comment.