Separated autocommit and isolation level handling for PostgreSQL.
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.
This commit is contained in:
parent
f515619494
commit
8717b0668c
|
@ -77,21 +77,21 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(DatabaseWrapper, self).__init__(*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)
|
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.ops = DatabaseOperations(self)
|
||||||
self.client = DatabaseClient(self)
|
self.client = DatabaseClient(self)
|
||||||
self.creation = DatabaseCreation(self)
|
self.creation = DatabaseCreation(self)
|
||||||
self.introspection = DatabaseIntrospection(self)
|
self.introspection = DatabaseIntrospection(self)
|
||||||
self.validation = BaseDatabaseValidation(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):
|
def get_connection_params(self):
|
||||||
settings_dict = self.settings_dict
|
settings_dict = self.settings_dict
|
||||||
if not settings_dict['NAME']:
|
if not settings_dict['NAME']:
|
||||||
|
@ -135,11 +135,12 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
|
|
||||||
if conn_tz != tz:
|
if conn_tz != tz:
|
||||||
# Set the time zone in autocommit mode (see #17062)
|
# Set the time zone in autocommit mode (see #17062)
|
||||||
self.connection.set_isolation_level(
|
self.set_autocommit(True)
|
||||||
psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
|
|
||||||
self.connection.cursor().execute(
|
self.connection.cursor().execute(
|
||||||
self.ops.set_time_zone_sql(), [tz])
|
self.ops.set_time_zone_sql(), [tz])
|
||||||
self.connection.set_isolation_level(self.isolation_level)
|
self.connection.set_isolation_level(self.isolation_level)
|
||||||
|
if self.features.uses_autocommit:
|
||||||
|
self.set_autocommit(True)
|
||||||
|
|
||||||
def create_cursor(self):
|
def create_cursor(self):
|
||||||
cursor = self.connection.cursor()
|
cursor = self.connection.cursor()
|
||||||
|
@ -172,42 +173,40 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
Switch the isolation level when needing transaction support, so that
|
Switch the isolation level when needing transaction support, so that
|
||||||
the same transaction is visible across all the queries.
|
the same transaction is visible across all the queries.
|
||||||
"""
|
"""
|
||||||
if self.features.uses_autocommit and managed and not self.isolation_level:
|
if self.connection is None: # Force creating a connection.
|
||||||
level = self.settings_dict["OPTIONS"].get('isolation_level',
|
self.cursor().close()
|
||||||
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED)
|
if self.features.uses_autocommit and managed and self.autocommit:
|
||||||
self._set_isolation_level(level)
|
self.set_autocommit(False)
|
||||||
|
self.features.uses_savepoints = True
|
||||||
|
|
||||||
def _leave_transaction_management(self, managed):
|
def _leave_transaction_management(self, managed):
|
||||||
"""
|
"""
|
||||||
If the normal operating mode is "autocommit", switch back to that when
|
If the normal operating mode is "autocommit", switch back to that when
|
||||||
leaving transaction management.
|
leaving transaction management.
|
||||||
"""
|
"""
|
||||||
if self.features.uses_autocommit and not managed and self.isolation_level:
|
if self.connection is None: # Force creating a connection.
|
||||||
self._set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
|
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, level):
|
def _set_isolation_level(self, isolation_level):
|
||||||
"""
|
assert isolation_level in range(1, 5) # Use set_autocommit for level = 0
|
||||||
Do all the related feature configurations for changing isolation
|
if self.psycopg2_version >= (2, 4, 2):
|
||||||
levels. This doesn't touch the uses_autocommit feature, since that
|
self.connection.set_session(isolation_level=isolation_level)
|
||||||
controls the movement *between* isolation levels.
|
else:
|
||||||
"""
|
self.connection.set_isolation_level(isolation_level)
|
||||||
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)
|
|
||||||
|
|
||||||
def _set_autocommit(self, autocommit):
|
def _set_autocommit(self, autocommit):
|
||||||
if autocommit:
|
if self.psycopg2_version >= (2, 4, 2):
|
||||||
level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
|
self.connection.autocommit = autocommit
|
||||||
else:
|
else:
|
||||||
level = self.settings_dict["OPTIONS"].get('isolation_level',
|
if autocommit:
|
||||||
psycopg2.extensions.ISOLATION_LEVEL_READ_COMMITTED)
|
level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
|
||||||
self._set_isolation_level(level)
|
else:
|
||||||
|
level = self.isolation_level
|
||||||
|
self.connection.set_isolation_level(level)
|
||||||
|
|
||||||
def set_dirty(self):
|
def set_dirty(self):
|
||||||
if ((self.transaction_state and self.transaction_state[-1]) or
|
if ((self.transaction_state and self.transaction_state[-1]) or
|
||||||
|
@ -231,6 +230,11 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
else:
|
else:
|
||||||
return True
|
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
|
@cached_property
|
||||||
def pg_version(self):
|
def pg_version(self):
|
||||||
with self.temporary_connection():
|
with self.temporary_connection():
|
||||||
|
|
|
@ -274,31 +274,39 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase):
|
||||||
connections[DEFAULT_DB_ALIAS] = self._old_backend
|
connections[DEFAULT_DB_ALIAS] = self._old_backend
|
||||||
|
|
||||||
def test_initial_autocommit_state(self):
|
def test_initial_autocommit_state(self):
|
||||||
|
# Autocommit is activated when the connection is created.
|
||||||
|
connection.cursor().close()
|
||||||
|
|
||||||
self.assertTrue(connection.features.uses_autocommit)
|
self.assertTrue(connection.features.uses_autocommit)
|
||||||
self.assertEqual(connection.isolation_level, self._autocommit)
|
self.assertTrue(connection.autocommit)
|
||||||
|
|
||||||
def test_transaction_management(self):
|
def test_transaction_management(self):
|
||||||
transaction.enter_transaction_management()
|
transaction.enter_transaction_management()
|
||||||
|
self.assertFalse(connection.autocommit)
|
||||||
self.assertEqual(connection.isolation_level, self._serializable)
|
self.assertEqual(connection.isolation_level, self._serializable)
|
||||||
|
|
||||||
transaction.leave_transaction_management()
|
transaction.leave_transaction_management()
|
||||||
self.assertEqual(connection.isolation_level, self._autocommit)
|
self.assertTrue(connection.autocommit)
|
||||||
|
|
||||||
def test_transaction_stacking(self):
|
def test_transaction_stacking(self):
|
||||||
transaction.enter_transaction_management()
|
transaction.enter_transaction_management()
|
||||||
|
self.assertFalse(connection.autocommit)
|
||||||
self.assertEqual(connection.isolation_level, self._serializable)
|
self.assertEqual(connection.isolation_level, self._serializable)
|
||||||
|
|
||||||
transaction.enter_transaction_management()
|
transaction.enter_transaction_management()
|
||||||
|
self.assertFalse(connection.autocommit)
|
||||||
self.assertEqual(connection.isolation_level, self._serializable)
|
self.assertEqual(connection.isolation_level, self._serializable)
|
||||||
|
|
||||||
transaction.leave_transaction_management()
|
transaction.leave_transaction_management()
|
||||||
|
self.assertFalse(connection.autocommit)
|
||||||
self.assertEqual(connection.isolation_level, self._serializable)
|
self.assertEqual(connection.isolation_level, self._serializable)
|
||||||
|
|
||||||
transaction.leave_transaction_management()
|
transaction.leave_transaction_management()
|
||||||
self.assertEqual(connection.isolation_level, self._autocommit)
|
self.assertTrue(connection.autocommit)
|
||||||
|
|
||||||
def test_enter_autocommit(self):
|
def test_enter_autocommit(self):
|
||||||
transaction.enter_transaction_management()
|
transaction.enter_transaction_management()
|
||||||
|
self.assertFalse(connection.autocommit)
|
||||||
self.assertEqual(connection.isolation_level, self._serializable)
|
self.assertEqual(connection.isolation_level, self._serializable)
|
||||||
list(Mod.objects.all())
|
list(Mod.objects.all())
|
||||||
self.assertTrue(transaction.is_dirty())
|
self.assertTrue(transaction.is_dirty())
|
||||||
|
@ -311,9 +319,10 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase):
|
||||||
list(Mod.objects.all())
|
list(Mod.objects.all())
|
||||||
self.assertFalse(transaction.is_dirty())
|
self.assertFalse(transaction.is_dirty())
|
||||||
transaction.leave_transaction_management()
|
transaction.leave_transaction_management()
|
||||||
|
self.assertFalse(connection.autocommit)
|
||||||
self.assertEqual(connection.isolation_level, self._serializable)
|
self.assertEqual(connection.isolation_level, self._serializable)
|
||||||
transaction.leave_transaction_management()
|
transaction.leave_transaction_management()
|
||||||
self.assertEqual(connection.isolation_level, self._autocommit)
|
self.assertTrue(connection.autocommit)
|
||||||
|
|
||||||
|
|
||||||
class TestManyToManyAddTransaction(TransactionTestCase):
|
class TestManyToManyAddTransaction(TransactionTestCase):
|
||||||
|
|
Loading…
Reference in New Issue