Skip to content

Commit c0a914f

Browse files
author
Matt George
committed
ISOLATION_LEVEL_SERIALIZABLE all the things
0 parents  commit c0a914f

File tree

8 files changed

+699
-0
lines changed

8 files changed

+699
-0
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*pyc
2+
*swp
3+
.DS_Store
4+
dist
5+
*.egg-info

django-redshift/__init__.py

Whitespace-only changes.

django-redshift/base.py

+242
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
"""
2+
PostgreSQL database backend for Django.
3+
4+
Requires psycopg 2: http://initd.org/projects/psycopg2
5+
"""
6+
import logging
7+
import sys
8+
9+
from django.db import utils
10+
from django.db.backends import *
11+
from django.db.backends.signals import connection_created
12+
from django.db.backends.postgresql_psycopg2.operations import DatabaseOperations
13+
from django.db.backends.postgresql_psycopg2.client import DatabaseClient
14+
from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation
15+
from django.db.backends.postgresql_psycopg2.version import get_version
16+
from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection
17+
from django.utils.encoding import force_str
18+
from django.utils.safestring import SafeText, SafeBytes
19+
from django.utils import six
20+
from django.utils.timezone import utc
21+
22+
try:
23+
import psycopg2 as Database
24+
import psycopg2.extensions
25+
except ImportError as e:
26+
from django.core.exceptions import ImproperlyConfigured
27+
raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e)
28+
29+
DatabaseError = Database.DatabaseError
30+
IntegrityError = Database.IntegrityError
31+
32+
psycopg2.extensions.register_type(psycopg2.extensions.UNICODE)
33+
psycopg2.extensions.register_adapter(SafeBytes, psycopg2.extensions.QuotedString)
34+
psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString)
35+
36+
logger = logging.getLogger('django.db.backends')
37+
38+
def utc_tzinfo_factory(offset):
39+
if offset != 0:
40+
raise AssertionError("database connection isn't set to UTC")
41+
return utc
42+
43+
class CursorWrapper(object):
44+
"""
45+
A thin wrapper around psycopg2's normal cursor class so that we can catch
46+
particular exception instances and reraise them with the right types.
47+
"""
48+
49+
def __init__(self, cursor):
50+
self.cursor = cursor
51+
52+
def execute(self, query, args=None):
53+
try:
54+
return self.cursor.execute(query, args)
55+
except Database.IntegrityError as e:
56+
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
57+
except Database.DatabaseError as e:
58+
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
59+
60+
def executemany(self, query, args):
61+
try:
62+
return self.cursor.executemany(query, args)
63+
except Database.IntegrityError as e:
64+
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])
65+
except Database.DatabaseError as e:
66+
six.reraise(utils.DatabaseError, utils.DatabaseError(*tuple(e.args)), sys.exc_info()[2])
67+
68+
def __getattr__(self, attr):
69+
if attr in self.__dict__:
70+
return self.__dict__[attr]
71+
else:
72+
return getattr(self.cursor, attr)
73+
74+
def __iter__(self):
75+
return iter(self.cursor)
76+
77+
class DatabaseFeatures(BaseDatabaseFeatures):
78+
needs_datetime_string_cast = False
79+
can_return_id_from_insert = True
80+
requires_rollback_on_dirty_transaction = True
81+
has_real_datatype = True
82+
can_defer_constraint_checks = True
83+
has_select_for_update = True
84+
has_select_for_update_nowait = True
85+
has_bulk_insert = True
86+
supports_tablespaces = True
87+
supports_transactions = True
88+
can_distinct_on_fields = True
89+
90+
class DatabaseWrapper(BaseDatabaseWrapper):
91+
vendor = 'postgresql'
92+
operators = {
93+
'exact': '= %s',
94+
'iexact': '= UPPER(%s)',
95+
'contains': 'LIKE %s',
96+
'icontains': 'LIKE UPPER(%s)',
97+
'regex': '~ %s',
98+
'iregex': '~* %s',
99+
'gt': '> %s',
100+
'gte': '>= %s',
101+
'lt': '< %s',
102+
'lte': '<= %s',
103+
'startswith': 'LIKE %s',
104+
'endswith': 'LIKE %s',
105+
'istartswith': 'LIKE UPPER(%s)',
106+
'iendswith': 'LIKE UPPER(%s)',
107+
}
108+
109+
def __init__(self, *args, **kwargs):
110+
super(DatabaseWrapper, self).__init__(*args, **kwargs)
111+
112+
self.features = DatabaseFeatures(self)
113+
autocommit = self.settings_dict["OPTIONS"].get('autocommit', False)
114+
self.features.uses_autocommit = autocommit
115+
#if autocommit:
116+
# level = psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT
117+
#else:
118+
level = psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE
119+
self._set_isolation_level(level)
120+
self.ops = DatabaseOperations(self)
121+
self.client = DatabaseClient(self)
122+
self.creation = DatabaseCreation(self)
123+
self.introspection = DatabaseIntrospection(self)
124+
self.validation = BaseDatabaseValidation(self)
125+
self._pg_version = None
126+
127+
def check_constraints(self, table_names=None):
128+
"""
129+
To check constraints, we set constraints to immediate. Then, when, we're done we must ensure they
130+
are returned to deferred.
131+
"""
132+
self.cursor().execute('SET CONSTRAINTS ALL IMMEDIATE')
133+
self.cursor().execute('SET CONSTRAINTS ALL DEFERRED')
134+
135+
def close(self):
136+
self.validate_thread_sharing()
137+
if self.connection is None:
138+
return
139+
140+
try:
141+
self.connection.close()
142+
self.connection = None
143+
except Database.Error:
144+
# In some cases (database restart, network connection lost etc...)
145+
# the connection to the database is lost without giving Django a
146+
# notification. If we don't set self.connection to None, the error
147+
# will occur a every request.
148+
self.connection = None
149+
logger.warning('psycopg2 error while closing the connection.',
150+
exc_info=sys.exc_info()
151+
)
152+
raise
153+
154+
def _get_pg_version(self):
155+
if self._pg_version is None:
156+
self._pg_version = get_version(self.connection)
157+
return self._pg_version
158+
pg_version = property(_get_pg_version)
159+
160+
def _cursor(self):
161+
settings_dict = self.settings_dict
162+
if self.connection is None:
163+
if not settings_dict['NAME']:
164+
from django.core.exceptions import ImproperlyConfigured
165+
raise ImproperlyConfigured(
166+
"settings.DATABASES is improperly configured. "
167+
"Please supply the NAME value.")
168+
conn_params = {
169+
'database': settings_dict['NAME'],
170+
}
171+
conn_params.update(settings_dict['OPTIONS'])
172+
if 'autocommit' in conn_params:
173+
del conn_params['autocommit']
174+
if settings_dict['USER']:
175+
conn_params['user'] = settings_dict['USER']
176+
if settings_dict['PASSWORD']:
177+
conn_params['password'] = force_str(settings_dict['PASSWORD'])
178+
if settings_dict['HOST']:
179+
conn_params['host'] = settings_dict['HOST']
180+
if settings_dict['PORT']:
181+
conn_params['port'] = settings_dict['PORT']
182+
self.connection = Database.connect(**conn_params)
183+
self.connection.set_client_encoding('UTF8')
184+
tz = 'UTC' if settings.USE_TZ else settings_dict.get('TIME_ZONE')
185+
if tz:
186+
try:
187+
get_parameter_status = self.connection.get_parameter_status
188+
except AttributeError:
189+
# psycopg2 < 2.0.12 doesn't have get_parameter_status
190+
conn_tz = None
191+
else:
192+
conn_tz = get_parameter_status('TimeZone')
193+
194+
if conn_tz != tz:
195+
# Set the time zone in autocommit mode (see #17062)
196+
self.connection.set_isolation_level(
197+
psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE)
198+
self.connection.cursor().execute(
199+
self.ops.set_time_zone_sql(), [tz])
200+
self.connection.set_isolation_level(self.isolation_level)
201+
self._get_pg_version()
202+
connection_created.send(sender=self.__class__, connection=self)
203+
cursor = self.connection.cursor()
204+
cursor.tzinfo_factory = utc_tzinfo_factory if settings.USE_TZ else None
205+
return CursorWrapper(cursor)
206+
207+
def _enter_transaction_management(self, managed):
208+
"""
209+
Switch the isolation level when needing transaction support, so that
210+
the same transaction is visible across all the queries.
211+
"""
212+
if self.features.uses_autocommit and managed and not self.isolation_level:
213+
self._set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE)
214+
215+
def _leave_transaction_management(self, managed):
216+
"""
217+
If the normal operating mode is "autocommit", switch back to that when
218+
leaving transaction management.
219+
"""
220+
if self.features.uses_autocommit and not managed and self.isolation_level:
221+
self._set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_SERIALIZABLE)
222+
223+
def _set_isolation_level(self, level):
224+
"""
225+
Do all the related feature configurations for changing isolation
226+
levels. This doesn't touch the uses_autocommit feature, since that
227+
controls the movement *between* isolation levels.
228+
"""
229+
assert level in range(5)
230+
try:
231+
if self.connection is not None:
232+
self.connection.set_isolation_level(level)
233+
finally:
234+
self.isolation_level = level
235+
self.features.uses_savepoints = bool(level)
236+
237+
def _commit(self):
238+
if self.connection is not None:
239+
try:
240+
return self.connection.commit()
241+
except Database.IntegrityError as e:
242+
six.reraise(utils.IntegrityError, utils.IntegrityError(*tuple(e.args)), sys.exc_info()[2])

django-redshift/client.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import os
2+
import sys
3+
4+
from django.db.backends import BaseDatabaseClient
5+
6+
class DatabaseClient(BaseDatabaseClient):
7+
executable_name = 'psql'
8+
9+
def runshell(self):
10+
settings_dict = self.connection.settings_dict
11+
args = [self.executable_name]
12+
if settings_dict['USER']:
13+
args += ["-U", settings_dict['USER']]
14+
if settings_dict['HOST']:
15+
args.extend(["-h", settings_dict['HOST']])
16+
if settings_dict['PORT']:
17+
args.extend(["-p", str(settings_dict['PORT'])])
18+
args += [settings_dict['NAME']]
19+
if os.name == 'nt':
20+
sys.exit(os.system(" ".join(args)))
21+
else:
22+
os.execvp(self.executable_name, args)
23+

django-redshift/creation.py

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import psycopg2.extensions
2+
3+
from django.db.backends.creation import BaseDatabaseCreation
4+
from django.db.backends.util import truncate_name
5+
6+
7+
class DatabaseCreation(BaseDatabaseCreation):
8+
# This dictionary maps Field objects to their associated PostgreSQL column
9+
# types, as strings. Column-type strings can contain format strings; they'll
10+
# be interpolated against the values of Field.__dict__ before being output.
11+
# If a column type is set to None, it won't be included in the output.
12+
data_types = {
13+
'AutoField': 'serial',
14+
'BooleanField': 'boolean',
15+
'CharField': 'varchar(%(max_length)s)',
16+
'CommaSeparatedIntegerField': 'varchar(%(max_length)s)',
17+
'DateField': 'date',
18+
'DateTimeField': 'timestamp with time zone',
19+
'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)',
20+
'FileField': 'varchar(%(max_length)s)',
21+
'FilePathField': 'varchar(%(max_length)s)',
22+
'FloatField': 'double precision',
23+
'IntegerField': 'integer',
24+
'BigIntegerField': 'bigint',
25+
'IPAddressField': 'inet',
26+
'GenericIPAddressField': 'inet',
27+
'NullBooleanField': 'boolean',
28+
'OneToOneField': 'integer',
29+
'PositiveIntegerField': 'integer CHECK ("%(column)s" >= 0)',
30+
'PositiveSmallIntegerField': 'smallint CHECK ("%(column)s" >= 0)',
31+
'SlugField': 'varchar(%(max_length)s)',
32+
'SmallIntegerField': 'smallint',
33+
'TextField': 'text',
34+
'TimeField': 'time',
35+
}
36+
37+
def sql_table_creation_suffix(self):
38+
assert self.connection.settings_dict['TEST_COLLATION'] is None, "PostgreSQL does not support collation setting at database creation time."
39+
if self.connection.settings_dict['TEST_CHARSET']:
40+
return "WITH ENCODING '%s'" % self.connection.settings_dict['TEST_CHARSET']
41+
return ''
42+
43+
def sql_indexes_for_field(self, model, f, style):
44+
output = []
45+
if f.db_index or f.unique:
46+
qn = self.connection.ops.quote_name
47+
db_table = model._meta.db_table
48+
tablespace = f.db_tablespace or model._meta.db_tablespace
49+
if tablespace:
50+
tablespace_sql = self.connection.ops.tablespace_sql(tablespace)
51+
if tablespace_sql:
52+
tablespace_sql = ' ' + tablespace_sql
53+
else:
54+
tablespace_sql = ''
55+
56+
def get_index_sql(index_name, opclass=''):
57+
return (style.SQL_KEYWORD('CREATE INDEX') + ' ' +
58+
style.SQL_TABLE(qn(truncate_name(index_name,self.connection.ops.max_name_length()))) + ' ' +
59+
style.SQL_KEYWORD('ON') + ' ' +
60+
style.SQL_TABLE(qn(db_table)) + ' ' +
61+
"(%s%s)" % (style.SQL_FIELD(qn(f.column)), opclass) +
62+
"%s;" % tablespace_sql)
63+
64+
if not f.unique:
65+
output = [get_index_sql('%s_%s' % (db_table, f.column))]
66+
67+
# Fields with database column types of `varchar` and `text` need
68+
# a second index that specifies their operator class, which is
69+
# needed when performing correct LIKE queries outside the
70+
# C locale. See #12234.
71+
db_type = f.db_type(connection=self.connection)
72+
if db_type.startswith('varchar'):
73+
output.append(get_index_sql('%s_%s_like' % (db_table, f.column),
74+
' varchar_pattern_ops'))
75+
elif db_type.startswith('text'):
76+
output.append(get_index_sql('%s_%s_like' % (db_table, f.column),
77+
' text_pattern_ops'))
78+
return output
79+
80+
def set_autocommit(self):
81+
self._prepare_for_test_db_ddl()
82+
83+
def _prepare_for_test_db_ddl(self):
84+
"""Rollback and close the active transaction."""
85+
self.connection.connection.rollback()
86+
self.connection.connection.set_isolation_level(
87+
psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)

0 commit comments

Comments
 (0)