Skip to content

Commit 4df37f3

Browse files
committed
provide compatibility with Django 1.11
1 parent 2137e1a commit 4df37f3

File tree

9 files changed

+253
-158
lines changed

9 files changed

+253
-158
lines changed

README.rst

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Microsoft SQL Server and Azure SQL Database.
1717
Features
1818
--------
1919

20-
- Supports Django 1.10.4
20+
- Supports Django 1.11.0
2121
- Supports Microsoft SQL Server 2005, 2008/2008R2, 2012, 2014, 2016 and
2222
Azure SQL Database
2323
- Supports LIMIT+OFFSET and offset w/o LIMIT emulation.
@@ -31,7 +31,7 @@ Features
3131
Dependencies
3232
------------
3333

34-
- Django 1.10.4
34+
- Django 1.11.0
3535
- pyodbc 3.0 or newer
3636

3737
Installation
@@ -234,16 +234,17 @@ Limitations
234234
The following features are currently not supported:
235235

236236
- Altering a model field from or to AutoField at migration
237+
- `Exists <https://docs.djangoproject.com/en/1.11/ref/models/expressions/#django.db.models.Exists>`__ subqueries
237238

238239
Notice
239240
------
240241

241-
This version of *django-pyodbc-azure* only supports Django 1.10.
242+
This version of *django-pyodbc-azure* only supports Django 1.11.
242243
If you want to use it on older versions of Django,
243-
specify an appropriate version number (1.9.x.x for Django 1.9)
244+
specify an appropriate version number (1.10.x.x for Django 1.10)
244245
at installation like this: ::
245246

246-
pip install "django-pyodbc-azure<1.10"
247+
pip install "django-pyodbc-azure<1.11"
247248

248249
License
249250
-------

setup.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@
1313
'Programming Language :: Python :: 3',
1414
'Programming Language :: Python :: 3.4',
1515
'Programming Language :: Python :: 3.5',
16+
'Programming Language :: Python :: 3.6',
1617
'Topic :: Internet :: WWW/HTTP',
1718
]
1819

1920
setup(
2021
name='django-pyodbc-azure',
21-
version='1.10.4.0',
22+
version='1.11.0.0',
2223
description='Django backend for Microsoft SQL Server and Azure SQL Database using pyodbc',
2324
long_description=open('README.rst').read(),
2425
author='Michiya Takahashi',
@@ -27,7 +28,7 @@
2728
license='BSD',
2829
packages=['sql_server', 'sql_server.pyodbc'],
2930
install_requires=[
30-
'Django>=1.10.4,<1.11',
31+
'Django>=1.11,<1.12',
3132
'pyodbc>=3.0',
3233
],
3334
classifiers=CLASSIFIERS,

sql_server/pyodbc/base.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
from django.core.exceptions import ImproperlyConfigured
99
from django import VERSION
10-
if VERSION[:3] < (1,10,4) or VERSION[:2] >= (1,11):
10+
if VERSION[:3] < (1,11,0) or VERSION[:2] >= (1,12):
1111
raise ImproperlyConfigured("Django %d.%d.%d is not supported." % VERSION[:3])
1212

1313
try:
@@ -31,12 +31,12 @@
3131
if not settings.DATABASE_CONNECTION_POOLING:
3232
Database.pooling = False
3333

34-
from sql_server.pyodbc.client import DatabaseClient
35-
from sql_server.pyodbc.creation import DatabaseCreation
36-
from sql_server.pyodbc.features import DatabaseFeatures
37-
from sql_server.pyodbc.introspection import DatabaseIntrospection
38-
from sql_server.pyodbc.operations import DatabaseOperations
39-
from sql_server.pyodbc.schema import DatabaseSchemaEditor
34+
from .client import DatabaseClient
35+
from .creation import DatabaseCreation
36+
from .features import DatabaseFeatures
37+
from .introspection import DatabaseIntrospection
38+
from .operations import DatabaseOperations
39+
from .schema import DatabaseSchemaEditor
4040

4141
EDITION_AZURE_SQL_DB = 5
4242

@@ -137,6 +137,12 @@ class DatabaseWrapper(BaseDatabaseWrapper):
137137

138138
Database = Database
139139
SchemaEditorClass = DatabaseSchemaEditor
140+
# Classes instantiated in __init__().
141+
client_class = DatabaseClient
142+
creation_class = DatabaseCreation
143+
features_class = DatabaseFeatures
144+
introspection_class = DatabaseIntrospection
145+
ops_class = DatabaseOperations
140146

141147
_codes_for_networkerror = (
142148
'08S01',
@@ -209,7 +215,7 @@ def __init__(self, *args, **kwargs):
209215
self.introspection = DatabaseIntrospection(self)
210216
self.validation = BaseDatabaseValidation(self)
211217

212-
def create_cursor(self):
218+
def create_cursor(self, name=None):
213219
return CursorWrapper(self.connection.cursor(), self)
214220

215221
def get_connection_params(self):

sql_server/pyodbc/compiler.py

Lines changed: 113 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from itertools import chain
22

33
from django.db.models.aggregates import Avg, Count, StdDev, Variance
4-
from django.db.models.expressions import Ref, Value
4+
from django.db.models.expressions import OrderBy, Ref, Value
55
from django.db.models.functions import ConcatPair, Greatest, Least, Length, Substr
66
from django.db.models.sql import compiler
77
from django.db.transaction import TransactionManagementError
@@ -39,6 +39,14 @@ def _as_sql_least(self, compiler, connection):
3939
def _as_sql_length(self, compiler, connection):
4040
return self.as_sql(compiler, connection, function='LEN')
4141

42+
def _as_sql_order_by(self, compiler, connection):
43+
template = None
44+
if self.nulls_last:
45+
template = 'CASE WHEN %(expression)s IS NULL THEN 1 ELSE 0 END, %(expression)s %(ordering)s'
46+
if self.nulls_first:
47+
template = 'CASE WHEN %(expression)s IS NULL THEN 0 ELSE 1 END, %(expression)s %(ordering)s'
48+
return self.as_sql(compiler, connection, template=template)
49+
4250
def _as_sql_stddev(self, compiler, connection):
4351
function = 'STDEV'
4452
if self.function == 'STDDEV_POP':
@@ -59,15 +67,14 @@ def _as_sql_variance(self, compiler, connection):
5967

6068
class SQLCompiler(compiler.SQLCompiler):
6169

62-
def as_sql(self, with_limits=True, with_col_aliases=False, subquery=False):
70+
def as_sql(self, with_limits=True, with_col_aliases=False):
6371
"""
6472
Creates the SQL for this query. Returns the SQL string and list of
6573
parameters.
6674
6775
If 'with_limits' is False, any limit/offset information is not included
6876
in the query.
6977
"""
70-
self.subquery = subquery
7178
refcounts_before = self.query.alias_refcount.copy()
7279
try:
7380
extra_select, order_by, group_by = self.pre_sql_setup()
@@ -88,94 +95,107 @@ def as_sql(self, with_limits=True, with_col_aliases=False, subquery=False):
8895
# docstring of get_from_clause() for details.
8996
from_, f_params = self.get_from_clause()
9097

98+
for_update_part = None
9199
where, w_params = self.compile(self.where) if self.where is not None else ("", [])
92100
having, h_params = self.compile(self.having) if self.having is not None else ("", [])
93-
params = []
94-
result = ['SELECT']
95-
96-
if self.query.distinct:
97-
result.append(self.connection.ops.distinct_sql(distinct_fields))
98-
99-
# SQL Server requires the keword for limitting at the begenning
100-
if do_limit and not do_offset:
101-
result.append('TOP %d' % high_mark)
102-
103-
out_cols = []
104-
col_idx = 1
105-
for _, (s_sql, s_params), alias in self.select + extra_select:
106-
if alias:
107-
s_sql = '%s AS %s' % (s_sql, self.connection.ops.quote_name(alias))
108-
elif with_col_aliases or do_offset_emulation:
109-
s_sql = '%s AS %s' % (s_sql, 'Col%d' % col_idx)
110-
col_idx += 1
111-
params.extend(s_params)
112-
out_cols.append(s_sql)
113-
114-
# SQL Server requires an order-by clause for offsetting
115-
if do_offset:
116-
meta = self.query.get_meta()
117-
qn = self.quote_name_unless_alias
118-
offsetting_order_by = '%s.%s' % (qn(meta.db_table), qn(meta.pk.db_column or meta.pk.column))
119-
if do_offset_emulation:
120-
if order_by:
121-
ordering = []
122-
for expr, (o_sql, o_params, _) in order_by:
123-
# value_expression in OVER clause cannot refer to
124-
# expressions or aliases in the select list. See:
125-
# http://msdn.microsoft.com/en-us/library/ms189461.aspx
126-
src = next(iter(expr.get_source_expressions()))
127-
if isinstance(src, Ref):
128-
src = next(iter(src.get_source_expressions()))
129-
o_sql, _ = src.as_sql(self, self.connection)
130-
odir = 'DESC' if expr.descending else 'ASC'
131-
o_sql = '%s %s' % (o_sql, odir)
132-
ordering.append(o_sql)
133-
params.extend(o_params)
134-
offsetting_order_by = ', '.join(ordering)
135-
order_by = []
136-
out_cols.append('ROW_NUMBER() OVER (ORDER BY %s) AS [rn]' % offsetting_order_by)
137-
elif not order_by:
138-
order_by.append(((None, ('%s ASC' % offsetting_order_by, [], None))))
139-
140-
result.append(', '.join(out_cols))
141-
142-
if self.query.select_for_update and self.connection.features.has_select_for_update:
143-
if self.connection.get_autocommit():
144-
raise TransactionManagementError(
145-
"select_for_update cannot be used outside of a transaction."
146-
)
147-
148-
# If we've been asked for a NOWAIT query but the backend does
149-
# not support it, raise a DatabaseError otherwise we could get
150-
# an unexpected deadlock.
151-
nowait = self.query.select_for_update_nowait
152-
if nowait and not self.connection.features.has_select_for_update_nowait:
153-
raise DatabaseError('NOWAIT is not supported on this database backend.')
154-
from_.insert(1, self.connection.ops.for_update_sql(nowait=nowait))
155-
156-
result.append('FROM')
157-
result.extend(from_)
158-
params.extend(f_params)
159-
160-
if where:
161-
result.append('WHERE %s' % where)
162-
params.extend(w_params)
163-
164-
grouping = []
165-
for g_sql, g_params in group_by:
166-
grouping.append(g_sql)
167-
params.extend(g_params)
168-
if grouping:
169-
if distinct_fields:
170-
raise NotImplementedError(
171-
"annotate() + distinct(fields) is not implemented.")
172-
if not order_by:
173-
order_by = self.connection.ops.force_no_ordering()
174-
result.append('GROUP BY %s' % ', '.join(grouping))
175-
176-
if having:
177-
result.append('HAVING %s' % having)
178-
params.extend(h_params)
101+
102+
combinator = self.query.combinator
103+
features = self.connection.features
104+
if combinator:
105+
if not getattr(features, 'supports_select_{}'.format(combinator)):
106+
raise DatabaseError('{} not supported on this database backend.'.format(combinator))
107+
result, params = self.get_combinator_sql(combinator, self.query.combinator_all)
108+
else:
109+
params = []
110+
result = ['SELECT']
111+
112+
if self.query.distinct:
113+
result.append(self.connection.ops.distinct_sql(distinct_fields))
114+
115+
# SQL Server requires the keword for limitting at the begenning
116+
if do_limit and not do_offset:
117+
result.append('TOP %d' % high_mark)
118+
119+
out_cols = []
120+
col_idx = 1
121+
for _, (s_sql, s_params), alias in self.select + extra_select:
122+
if alias:
123+
s_sql = '%s AS %s' % (s_sql, self.connection.ops.quote_name(alias))
124+
elif with_col_aliases or do_offset_emulation:
125+
s_sql = '%s AS %s' % (s_sql, 'Col%d' % col_idx)
126+
col_idx += 1
127+
params.extend(s_params)
128+
out_cols.append(s_sql)
129+
130+
# SQL Server requires an order-by clause for offsetting
131+
if do_offset:
132+
meta = self.query.get_meta()
133+
qn = self.quote_name_unless_alias
134+
offsetting_order_by = '%s.%s' % (qn(meta.db_table), qn(meta.pk.db_column or meta.pk.column))
135+
if do_offset_emulation:
136+
if order_by:
137+
ordering = []
138+
for expr, (o_sql, o_params, _) in order_by:
139+
# value_expression in OVER clause cannot refer to
140+
# expressions or aliases in the select list. See:
141+
# http://msdn.microsoft.com/en-us/library/ms189461.aspx
142+
src = next(iter(expr.get_source_expressions()))
143+
if isinstance(src, Ref):
144+
src = next(iter(src.get_source_expressions()))
145+
o_sql, _ = src.as_sql(self, self.connection)
146+
odir = 'DESC' if expr.descending else 'ASC'
147+
o_sql = '%s %s' % (o_sql, odir)
148+
ordering.append(o_sql)
149+
params.extend(o_params)
150+
offsetting_order_by = ', '.join(ordering)
151+
order_by = []
152+
out_cols.append('ROW_NUMBER() OVER (ORDER BY %s) AS [rn]' % offsetting_order_by)
153+
elif not order_by:
154+
order_by.append(((None, ('%s ASC' % offsetting_order_by, [], None))))
155+
156+
result.append(', '.join(out_cols))
157+
158+
if self.query.select_for_update and self.connection.features.has_select_for_update:
159+
if self.connection.get_autocommit():
160+
raise TransactionManagementError('select_for_update cannot be used outside of a transaction.')
161+
162+
nowait = self.query.select_for_update_nowait
163+
skip_locked = self.query.select_for_update_skip_locked
164+
# If it's a NOWAIT/SKIP LOCKED query but the backend
165+
# doesn't support it, raise a DatabaseError to prevent a
166+
# possible deadlock.
167+
if nowait and not self.connection.features.has_select_for_update_nowait:
168+
raise DatabaseError('NOWAIT is not supported on this database backend.')
169+
elif skip_locked and not self.connection.features.has_select_for_update_skip_locked:
170+
raise DatabaseError('SKIP LOCKED is not supported on this database backend.')
171+
for_update_part = self.connection.ops.for_update_sql(nowait=nowait, skip_locked=skip_locked)
172+
173+
if for_update_part and self.connection.features.for_update_after_from:
174+
from_.insert(1, for_update_part)
175+
176+
result.append('FROM')
177+
result.extend(from_)
178+
params.extend(f_params)
179+
180+
if where:
181+
result.append('WHERE %s' % where)
182+
params.extend(w_params)
183+
184+
grouping = []
185+
for g_sql, g_params in group_by:
186+
grouping.append(g_sql)
187+
params.extend(g_params)
188+
if grouping:
189+
if distinct_fields:
190+
raise NotImplementedError(
191+
"annotate() + distinct(fields) is not implemented.")
192+
if not order_by:
193+
order_by = self.connection.ops.force_no_ordering()
194+
result.append('GROUP BY %s' % ', '.join(grouping))
195+
196+
if having:
197+
result.append('HAVING %s' % having)
198+
params.extend(h_params)
179199

180200
if order_by:
181201
ordering = []
@@ -196,7 +216,7 @@ def as_sql(self, with_limits=True, with_col_aliases=False, subquery=False):
196216
result.append('BETWEEN %d AND %d' % (low_mark+1, high_mark))
197217
else:
198218
result.append('>= %d' % (low_mark+1))
199-
if not subquery:
219+
if not self.query.subquery:
200220
result.append('ORDER BY X.rn')
201221
else:
202222
result.append('OFFSET %d ROWS' % low_mark)
@@ -226,6 +246,8 @@ def _as_microsoft(self, node):
226246
as_microsoft = _as_sql_least
227247
elif isinstance(node, Length):
228248
as_microsoft = _as_sql_length
249+
elif isinstance(node, OrderBy):
250+
as_microsoft = _as_sql_order_by
229251
elif isinstance(node, StdDev):
230252
as_microsoft = _as_sql_stddev
231253
elif isinstance(node, Substr):
@@ -288,7 +310,7 @@ def as_sql(self):
288310
]
289311

290312
if has_fields:
291-
if opts.has_auto_field:
313+
if opts.auto_field is not None:
292314
# db_column is None if not explicitly specified by model field
293315
auto_field_column = opts.auto_field.db_column or opts.auto_field.column
294316
columns = [f.column for f in fields]

sql_server/pyodbc/creation.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import time
2-
31
from django.db.backends.base.creation import BaseDatabaseCreation
42

53

@@ -14,8 +12,6 @@ def _destroy_test_db(self, test_database_name, verbosity):
1412
# to do so, because it's not allowed to delete a database while being
1513
# connected to it.
1614
with self.connection._nodb_connection.cursor() as cursor:
17-
# Wait to avoid "database is being accessed by other users" errors.
18-
time.sleep(1)
1915
to_azure_sql_db = self.connection.to_azure_sql_db
2016
if not to_azure_sql_db:
2117
cursor.execute("ALTER DATABASE %s SET SINGLE_USER WITH ROLLBACK IMMEDIATE"

sql_server/pyodbc/features.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
1212
can_introspect_small_integer_field = True
1313
can_return_id_from_insert = True
1414
can_use_chunked_reads = False
15+
for_update_after_from = True
1516
has_bulk_insert = True
1617
has_real_datatype = True
1718
has_select_for_update = True

0 commit comments

Comments
 (0)