From 76356d963c131252e1ce4285083fd21fd0bdedd9 Mon Sep 17 00:00:00 2001 From: Aymeric Augustin Date: Sat, 14 Feb 2015 09:50:38 +0100 Subject: [PATCH] Fixed #24318 -- Set the transaction isolation level with psycopg >= 2.4.2. --- .../db/backends/postgresql_psycopg2/base.py | 28 +++++++++++++++---- docs/releases/1.7.5.txt | 4 +++ tests/backends/tests.py | 28 +++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index 4410a173a03..d2e114fea57 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -127,10 +127,6 @@ 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) self.ops = DatabaseOperations(self) self.client = DatabaseClient(self) @@ -165,7 +161,29 @@ class DatabaseWrapper(BaseDatabaseWrapper): return conn_params def get_new_connection(self, conn_params): - return Database.connect(**conn_params) + connection = Database.connect(**conn_params) + + # self.isolation_level must be set: + # - after connecting to the database in order to obtain the database's + # default when no value is explicitly specified in options. + # - before calling _set_autocommit() because if autocommit is on, that + # will set connection.isolation_level to ISOLATION_LEVEL_AUTOCOMMIT; + # and if autocommit is off, on psycopg2 < 2.4.2, _set_autocommit() + # needs self.isolation_level. + options = self.settings_dict['OPTIONS'] + try: + self.isolation_level = options['isolation_level'] + except KeyError: + self.isolation_level = connection.isolation_level + else: + # Set the isolation level to the value from OPTIONS. This isn't + # needed on psycopg2 < 2.4.2 because it happens as a side-effect + # of _set_autocommit(False). + if (self.isolation_level != connection.isolation_level and + self.psycopg2_version >= (2, 4, 2)): + connection.set_session(isolation_level=self.isolation_level) + + return connection def init_connection_state(self): settings_dict = self.settings_dict diff --git a/docs/releases/1.7.5.txt b/docs/releases/1.7.5.txt index 604cf9146ae..432031ef38f 100644 --- a/docs/releases/1.7.5.txt +++ b/docs/releases/1.7.5.txt @@ -19,3 +19,7 @@ Bugfixes * Fixed crash in ``contrib.sites`` migrations when a default database isn't used (:ticket:`24332`). + +* Added the ability to set the isolation level on PostgreSQL with psycopg2 ≥ + 2.4.2 (:ticket:`24318`). It was advertised as a new feature in Django 1.6 + but it didn't work in practice. diff --git a/tests/backends/tests.py b/tests/backends/tests.py index 8762d263b94..52c6d44051c 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -237,6 +237,34 @@ class PostgreSQLTests(TestCase): finally: new_connection.close() + def test_connect_isolation_level(self): + """ + Regression test for #18130 and #24318. + """ + from psycopg2.extensions import ( + ISOLATION_LEVEL_READ_COMMITTED as read_committed, + ISOLATION_LEVEL_SERIALIZABLE as serializable, + ) + + # Since this is a django.test.TestCase, a transaction is in progress + # and the isolation level isn't reported as 0. This test assumes that + # PostgreSQL is configured with the default isolation level. + + # Check the level on the psycopg2 connection, not the Django wrapper. + self.assertEqual(connection.connection.isolation_level, read_committed) + + databases = copy.deepcopy(settings.DATABASES) + databases[DEFAULT_DB_ALIAS]['OPTIONS']['isolation_level'] = serializable + new_connections = ConnectionHandler(databases) + new_connection = new_connections[DEFAULT_DB_ALIAS] + try: + # Start a transaction so the isolation level isn't reported as 0. + new_connection.set_autocommit(False) + # Check the level on the psycopg2 connection, not the Django wrapper. + self.assertEqual(new_connection.connection.isolation_level, serializable) + finally: + new_connection.close() + def _select(self, val): with connection.cursor() as cursor: cursor.execute("SELECT %s", (val,))