Skip to content

Commit

Permalink
provide compatibility with Django 1.11
Browse files Browse the repository at this point in the history
  • Loading branch information
michiya committed Apr 25, 2017
1 parent 2137e1a commit 4df37f3
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 158 deletions.
11 changes: 6 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Microsoft SQL Server and Azure SQL Database.
Features
--------

- Supports Django 1.10.4
- Supports Django 1.11.0
- Supports Microsoft SQL Server 2005, 2008/2008R2, 2012, 2014, 2016 and
Azure SQL Database
- Supports LIMIT+OFFSET and offset w/o LIMIT emulation.
Expand All @@ -31,7 +31,7 @@ Features
Dependencies
------------

- Django 1.10.4
- Django 1.11.0
- pyodbc 3.0 or newer

Installation
Expand Down Expand Up @@ -234,16 +234,17 @@ Limitations
The following features are currently not supported:

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

Notice
------

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

pip install "django-pyodbc-azure<1.10"
pip install "django-pyodbc-azure<1.11"

License
-------
Expand Down
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Internet :: WWW/HTTP',
]

setup(
name='django-pyodbc-azure',
version='1.10.4.0',
version='1.11.0.0',
description='Django backend for Microsoft SQL Server and Azure SQL Database using pyodbc',
long_description=open('README.rst').read(),
author='Michiya Takahashi',
Expand All @@ -27,7 +28,7 @@
license='BSD',
packages=['sql_server', 'sql_server.pyodbc'],
install_requires=[
'Django>=1.10.4,<1.11',
'Django>=1.11,<1.12',
'pyodbc>=3.0',
],
classifiers=CLASSIFIERS,
Expand Down
22 changes: 14 additions & 8 deletions sql_server/pyodbc/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

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

try:
Expand All @@ -31,12 +31,12 @@
if not settings.DATABASE_CONNECTION_POOLING:
Database.pooling = False

from sql_server.pyodbc.client import DatabaseClient
from sql_server.pyodbc.creation import DatabaseCreation
from sql_server.pyodbc.features import DatabaseFeatures
from sql_server.pyodbc.introspection import DatabaseIntrospection
from sql_server.pyodbc.operations import DatabaseOperations
from sql_server.pyodbc.schema import DatabaseSchemaEditor
from .client import DatabaseClient
from .creation import DatabaseCreation
from .features import DatabaseFeatures
from .introspection import DatabaseIntrospection
from .operations import DatabaseOperations
from .schema import DatabaseSchemaEditor

EDITION_AZURE_SQL_DB = 5

Expand Down Expand Up @@ -137,6 +137,12 @@ class DatabaseWrapper(BaseDatabaseWrapper):

Database = Database
SchemaEditorClass = DatabaseSchemaEditor
# Classes instantiated in __init__().
client_class = DatabaseClient
creation_class = DatabaseCreation
features_class = DatabaseFeatures
introspection_class = DatabaseIntrospection
ops_class = DatabaseOperations

_codes_for_networkerror = (
'08S01',
Expand Down Expand Up @@ -209,7 +215,7 @@ def __init__(self, *args, **kwargs):
self.introspection = DatabaseIntrospection(self)
self.validation = BaseDatabaseValidation(self)

def create_cursor(self):
def create_cursor(self, name=None):
return CursorWrapper(self.connection.cursor(), self)

def get_connection_params(self):
Expand Down
204 changes: 113 additions & 91 deletions sql_server/pyodbc/compiler.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from itertools import chain

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

def _as_sql_order_by(self, compiler, connection):
template = None
if self.nulls_last:
template = 'CASE WHEN %(expression)s IS NULL THEN 1 ELSE 0 END, %(expression)s %(ordering)s'
if self.nulls_first:
template = 'CASE WHEN %(expression)s IS NULL THEN 0 ELSE 1 END, %(expression)s %(ordering)s'
return self.as_sql(compiler, connection, template=template)

def _as_sql_stddev(self, compiler, connection):
function = 'STDEV'
if self.function == 'STDDEV_POP':
Expand All @@ -59,15 +67,14 @@ def _as_sql_variance(self, compiler, connection):

class SQLCompiler(compiler.SQLCompiler):

def as_sql(self, with_limits=True, with_col_aliases=False, subquery=False):
def as_sql(self, with_limits=True, with_col_aliases=False):
"""
Creates the SQL for this query. Returns the SQL string and list of
parameters.
If 'with_limits' is False, any limit/offset information is not included
in the query.
"""
self.subquery = subquery
refcounts_before = self.query.alias_refcount.copy()
try:
extra_select, order_by, group_by = self.pre_sql_setup()
Expand All @@ -88,94 +95,107 @@ def as_sql(self, with_limits=True, with_col_aliases=False, subquery=False):
# docstring of get_from_clause() for details.
from_, f_params = self.get_from_clause()

for_update_part = None
where, w_params = self.compile(self.where) if self.where is not None else ("", [])
having, h_params = self.compile(self.having) if self.having is not None else ("", [])
params = []
result = ['SELECT']

if self.query.distinct:
result.append(self.connection.ops.distinct_sql(distinct_fields))

# SQL Server requires the keword for limitting at the begenning
if do_limit and not do_offset:
result.append('TOP %d' % high_mark)

out_cols = []
col_idx = 1
for _, (s_sql, s_params), alias in self.select + extra_select:
if alias:
s_sql = '%s AS %s' % (s_sql, self.connection.ops.quote_name(alias))
elif with_col_aliases or do_offset_emulation:
s_sql = '%s AS %s' % (s_sql, 'Col%d' % col_idx)
col_idx += 1
params.extend(s_params)
out_cols.append(s_sql)

# SQL Server requires an order-by clause for offsetting
if do_offset:
meta = self.query.get_meta()
qn = self.quote_name_unless_alias
offsetting_order_by = '%s.%s' % (qn(meta.db_table), qn(meta.pk.db_column or meta.pk.column))
if do_offset_emulation:
if order_by:
ordering = []
for expr, (o_sql, o_params, _) in order_by:
# value_expression in OVER clause cannot refer to
# expressions or aliases in the select list. See:
# http://msdn.microsoft.com/en-us/library/ms189461.aspx
src = next(iter(expr.get_source_expressions()))
if isinstance(src, Ref):
src = next(iter(src.get_source_expressions()))
o_sql, _ = src.as_sql(self, self.connection)
odir = 'DESC' if expr.descending else 'ASC'
o_sql = '%s %s' % (o_sql, odir)
ordering.append(o_sql)
params.extend(o_params)
offsetting_order_by = ', '.join(ordering)
order_by = []
out_cols.append('ROW_NUMBER() OVER (ORDER BY %s) AS [rn]' % offsetting_order_by)
elif not order_by:
order_by.append(((None, ('%s ASC' % offsetting_order_by, [], None))))

result.append(', '.join(out_cols))

if self.query.select_for_update and self.connection.features.has_select_for_update:
if self.connection.get_autocommit():
raise TransactionManagementError(
"select_for_update cannot be used outside of a transaction."
)

# If we've been asked for a NOWAIT query but the backend does
# not support it, raise a DatabaseError otherwise we could get
# an unexpected deadlock.
nowait = self.query.select_for_update_nowait
if nowait and not self.connection.features.has_select_for_update_nowait:
raise DatabaseError('NOWAIT is not supported on this database backend.')
from_.insert(1, self.connection.ops.for_update_sql(nowait=nowait))

result.append('FROM')
result.extend(from_)
params.extend(f_params)

if where:
result.append('WHERE %s' % where)
params.extend(w_params)

grouping = []
for g_sql, g_params in group_by:
grouping.append(g_sql)
params.extend(g_params)
if grouping:
if distinct_fields:
raise NotImplementedError(
"annotate() + distinct(fields) is not implemented.")
if not order_by:
order_by = self.connection.ops.force_no_ordering()
result.append('GROUP BY %s' % ', '.join(grouping))

if having:
result.append('HAVING %s' % having)
params.extend(h_params)

combinator = self.query.combinator
features = self.connection.features
if combinator:
if not getattr(features, 'supports_select_{}'.format(combinator)):
raise DatabaseError('{} not supported on this database backend.'.format(combinator))
result, params = self.get_combinator_sql(combinator, self.query.combinator_all)
else:
params = []
result = ['SELECT']

if self.query.distinct:
result.append(self.connection.ops.distinct_sql(distinct_fields))

# SQL Server requires the keword for limitting at the begenning
if do_limit and not do_offset:
result.append('TOP %d' % high_mark)

out_cols = []
col_idx = 1
for _, (s_sql, s_params), alias in self.select + extra_select:
if alias:
s_sql = '%s AS %s' % (s_sql, self.connection.ops.quote_name(alias))
elif with_col_aliases or do_offset_emulation:
s_sql = '%s AS %s' % (s_sql, 'Col%d' % col_idx)
col_idx += 1
params.extend(s_params)
out_cols.append(s_sql)

# SQL Server requires an order-by clause for offsetting
if do_offset:
meta = self.query.get_meta()
qn = self.quote_name_unless_alias
offsetting_order_by = '%s.%s' % (qn(meta.db_table), qn(meta.pk.db_column or meta.pk.column))
if do_offset_emulation:
if order_by:
ordering = []
for expr, (o_sql, o_params, _) in order_by:
# value_expression in OVER clause cannot refer to
# expressions or aliases in the select list. See:
# http://msdn.microsoft.com/en-us/library/ms189461.aspx
src = next(iter(expr.get_source_expressions()))
if isinstance(src, Ref):
src = next(iter(src.get_source_expressions()))
o_sql, _ = src.as_sql(self, self.connection)
odir = 'DESC' if expr.descending else 'ASC'
o_sql = '%s %s' % (o_sql, odir)
ordering.append(o_sql)
params.extend(o_params)
offsetting_order_by = ', '.join(ordering)
order_by = []
out_cols.append('ROW_NUMBER() OVER (ORDER BY %s) AS [rn]' % offsetting_order_by)
elif not order_by:
order_by.append(((None, ('%s ASC' % offsetting_order_by, [], None))))

result.append(', '.join(out_cols))

if self.query.select_for_update and self.connection.features.has_select_for_update:
if self.connection.get_autocommit():
raise TransactionManagementError('select_for_update cannot be used outside of a transaction.')

nowait = self.query.select_for_update_nowait
skip_locked = self.query.select_for_update_skip_locked
# If it's a NOWAIT/SKIP LOCKED query but the backend
# doesn't support it, raise a DatabaseError to prevent a
# possible deadlock.
if nowait and not self.connection.features.has_select_for_update_nowait:
raise DatabaseError('NOWAIT is not supported on this database backend.')
elif skip_locked and not self.connection.features.has_select_for_update_skip_locked:
raise DatabaseError('SKIP LOCKED is not supported on this database backend.')
for_update_part = self.connection.ops.for_update_sql(nowait=nowait, skip_locked=skip_locked)

if for_update_part and self.connection.features.for_update_after_from:
from_.insert(1, for_update_part)

result.append('FROM')
result.extend(from_)
params.extend(f_params)

if where:
result.append('WHERE %s' % where)
params.extend(w_params)

grouping = []
for g_sql, g_params in group_by:
grouping.append(g_sql)
params.extend(g_params)
if grouping:
if distinct_fields:
raise NotImplementedError(
"annotate() + distinct(fields) is not implemented.")
if not order_by:
order_by = self.connection.ops.force_no_ordering()
result.append('GROUP BY %s' % ', '.join(grouping))

if having:
result.append('HAVING %s' % having)
params.extend(h_params)

if order_by:
ordering = []
Expand All @@ -196,7 +216,7 @@ def as_sql(self, with_limits=True, with_col_aliases=False, subquery=False):
result.append('BETWEEN %d AND %d' % (low_mark+1, high_mark))
else:
result.append('>= %d' % (low_mark+1))
if not subquery:
if not self.query.subquery:
result.append('ORDER BY X.rn')
else:
result.append('OFFSET %d ROWS' % low_mark)
Expand Down Expand Up @@ -226,6 +246,8 @@ def _as_microsoft(self, node):
as_microsoft = _as_sql_least
elif isinstance(node, Length):
as_microsoft = _as_sql_length
elif isinstance(node, OrderBy):
as_microsoft = _as_sql_order_by
elif isinstance(node, StdDev):
as_microsoft = _as_sql_stddev
elif isinstance(node, Substr):
Expand Down Expand Up @@ -288,7 +310,7 @@ def as_sql(self):
]

if has_fields:
if opts.has_auto_field:
if opts.auto_field is not None:
# db_column is None if not explicitly specified by model field
auto_field_column = opts.auto_field.db_column or opts.auto_field.column
columns = [f.column for f in fields]
Expand Down
4 changes: 0 additions & 4 deletions sql_server/pyodbc/creation.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import time

from django.db.backends.base.creation import BaseDatabaseCreation


Expand All @@ -14,8 +12,6 @@ def _destroy_test_db(self, test_database_name, verbosity):
# to do so, because it's not allowed to delete a database while being
# connected to it.
with self.connection._nodb_connection.cursor() as cursor:
# Wait to avoid "database is being accessed by other users" errors.
time.sleep(1)
to_azure_sql_db = self.connection.to_azure_sql_db
if not to_azure_sql_db:
cursor.execute("ALTER DATABASE %s SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
Expand Down
1 change: 1 addition & 0 deletions sql_server/pyodbc/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
can_introspect_small_integer_field = True
can_return_id_from_insert = True
can_use_chunked_reads = False
for_update_after_from = True
has_bulk_insert = True
has_real_datatype = True
has_select_for_update = True
Expand Down
Loading

0 comments on commit 4df37f3

Please sign in to comment.