Skip to content

Commit 377f1c0

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents 70c17d5 + 7982506 commit 377f1c0

File tree

9 files changed

+141
-54
lines changed

9 files changed

+141
-54
lines changed

.github/workflows/main.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,14 @@ jobs:
4141
tox_env:
4242
- "py36-django22"
4343
- "py36-django30"
44+
- "py36-django31"
4445

4546
- "py37-django22"
4647
- "py37-django30"
48+
- "py37-django31"
4749

4850
- "py38-django30"
51+
- "py38-django31"
4952

5053
include:
5154
- python: "3.6"
@@ -54,15 +57,24 @@ jobs:
5457
- python: "3.6"
5558
tox_env: "py36-django30"
5659

60+
- python: "3.6"
61+
tox_env: "py36-django31"
62+
5763
- python: "3.7"
5864
tox_env: "py37-django22"
5965

6066
- python: "3.7"
6167
tox_env: "py37-django30"
6268

69+
- python: "3.7"
70+
tox_env: "py37-django31"
71+
6372
- python: "3.8"
6473
tox_env: "py38-django30"
6574

75+
- python: "3.8"
76+
tox_env: "py38-django31"
77+
6678

6779
steps:
6880
- uses: actions/checkout@v2

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,19 +41,25 @@ matrix:
4141

4242
- { before_install: *linux_before_install, python: "3.6", os: linux, env: TOX_ENV=py36-django22 }
4343
- { before_install: *linux_before_install, python: "3.6", os: linux, env: TOX_ENV=py36-django30 }
44+
- { before_install: *linux_before_install, python: "3.6", os: linux, env: TOX_ENV=py36-django31 }
4445

4546
- { before_install: *linux_before_install, python: "3.7", os: linux, env: TOX_ENV=py37-django22 }
4647
- { before_install: *linux_before_install, python: "3.7", os: linux, env: TOX_ENV=py37-django30 }
48+
- { before_install: *linux_before_install, python: "3.7", os: linux, env: TOX_ENV=py37-django31 }
4749

4850
- { before_install: *linux_before_install, python: "3.8", os: linux, env: TOX_ENV=py38-django30 }
51+
- { before_install: *linux_before_install, python: "3.8", os: linux, env: TOX_ENV=py38-django31 }
4952

5053
- { before_install: *win_before_install, language: sh, python: "3.6", os: windows, env: TOX_ENV=py36-django22 }
5154
- { before_install: *win_before_install, language: sh, python: "3.6", os: windows, env: TOX_ENV=py36-django30 }
55+
- { before_install: *win_before_install, language: sh, python: "3.6", os: windows, env: TOX_ENV=py36-django31 }
5256

5357
- { before_install: *win_before_install, language: sh, python: "3.7", os: windows, env: TOX_ENV=py37-django22 }
5458
- { before_install: *win_before_install, language: sh, python: "3.7", os: windows, env: TOX_ENV=py37-django30 }
59+
- { before_install: *win_before_install, language: sh, python: "3.7", os: windows, env: TOX_ENV=py37-django31 }
5560

5661
- { before_install: *win_before_install, language: sh, python: "3.8", os: windows, env: TOX_ENV=py38-django30 }
62+
- { before_install: *win_before_install, language: sh, python: "3.8", os: windows, env: TOX_ENV=py38-django31 }
5763

5864

5965

sql_server/pyodbc/base.py

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,28 +12,28 @@
1212
except ImportError as e:
1313
raise ImproperlyConfigured("Error loading pyodbc module: %s" % e)
1414

15-
from django.utils.version import get_version_tuple # noqa
15+
from django.utils.version import get_version_tuple # noqa
1616

1717
pyodbc_ver = get_version_tuple(Database.version)
1818
if pyodbc_ver < (3, 0):
1919
raise ImproperlyConfigured("pyodbc 3.0 or newer is required; you have %s" % Database.version)
2020

21-
from django.conf import settings # noqa
22-
from django.db import NotSupportedError # noqa
23-
from django.db.backends.base.base import BaseDatabaseWrapper # noqa
24-
from django.utils.encoding import smart_str # noqa
25-
from django.utils.functional import cached_property # noqa
21+
from django.conf import settings # noqa
22+
from django.db import NotSupportedError # noqa
23+
from django.db.backends.base.base import BaseDatabaseWrapper # noqa
24+
from django.utils.encoding import smart_str # noqa
25+
from django.utils.functional import cached_property # noqa
2626

2727
if hasattr(settings, 'DATABASE_CONNECTION_POOLING'):
2828
if not settings.DATABASE_CONNECTION_POOLING:
2929
Database.pooling = False
3030

31-
from .client import DatabaseClient # noqa
32-
from .creation import DatabaseCreation # noqa
33-
from .features import DatabaseFeatures # noqa
34-
from .introspection import DatabaseIntrospection # noqa
35-
from .operations import DatabaseOperations # noqa
36-
from .schema import DatabaseSchemaEditor # noqa
31+
from .client import DatabaseClient # noqa
32+
from .creation import DatabaseCreation # noqa
33+
from .features import DatabaseFeatures # noqa
34+
from .introspection import DatabaseIntrospection # noqa
35+
from .operations import DatabaseOperations # noqa
36+
from .schema import DatabaseSchemaEditor # noqa
3737

3838
EDITION_AZURE_SQL_DB = 5
3939

@@ -95,6 +95,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
9595
'TextField': 'nvarchar(max)',
9696
'TimeField': 'time',
9797
'UUIDField': 'char(32)',
98+
'JSONField': 'nvarchar(max)',
9899
}
99100
data_type_check_constraints = {
100101
'PositiveIntegerField': '[%(column)s] >= 0',

sql_server/pyodbc/creation.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import binascii
22
import os
33

4+
import django
45
from django.db.backends.base.creation import BaseDatabaseCreation
56

67

78
class DatabaseCreation(BaseDatabaseCreation):
9+
@property
10+
def cursor(self):
11+
if django.VERSION >= (3, 1):
12+
return self.connection._nodb_cursor
13+
14+
return self.connection._nodb_connection.cursor
815

916
def _destroy_test_db(self, test_database_name, verbosity):
1017
"""
@@ -14,7 +21,7 @@ def _destroy_test_db(self, test_database_name, verbosity):
1421
# ourselves. Connect to the previous database (not the test database)
1522
# to do so, because it's not allowed to delete a database while being
1623
# connected to it.
17-
with self.connection._nodb_connection.cursor() as cursor:
24+
with self.cursor() as cursor:
1825
to_azure_sql_db = self.connection.to_azure_sql_db
1926
if not to_azure_sql_db:
2027
cursor.execute("ALTER DATABASE %s SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
@@ -36,7 +43,7 @@ def enable_clr(self):
3643
This function will not fail if current user doesn't have
3744
permissions to enable clr, and clr is already enabled
3845
"""
39-
with self._nodb_connection.cursor() as cursor:
46+
with self.cursor() as cursor:
4047
# check whether clr is enabled
4148
cursor.execute('''
4249
SELECT value FROM sys.configurations
@@ -86,7 +93,7 @@ def install_regex_clr(self, database_name):
8693

8794
self.enable_clr()
8895

89-
with self._nodb_connection.cursor() as cursor:
96+
with self.cursor() as cursor:
9097
for s in sql:
9198
cursor.execute(s)
9299

sql_server/pyodbc/features.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
2222
requires_literal_defaults = True
2323
requires_sqlparse_for_splitting = False
2424
supports_boolean_expr_in_select_clause = False
25+
supports_deferrable_unique_constraints = False
2526
supports_ignore_conflicts = False
2627
supports_index_on_text_field = False
2728
supports_paramstyle_pyformat = False
@@ -33,6 +34,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
3334
supports_timezones = False
3435
supports_transactions = True
3536
uses_savepoints = True
37+
supports_order_by_nulls_modifier = False
38+
supports_order_by_is_nulls = False
3639

3740
@cached_property
3841
def has_bulk_insert(self):

sql_server/pyodbc/functions.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,34 @@ class TryCast(Cast):
1212
function = 'TRY_CAST'
1313

1414

15+
def sqlserver_as_sql(self, compiler, connection, template=None, **extra_context):
16+
template = template or self.template
17+
if connection.features.supports_order_by_nulls_modifier:
18+
if self.nulls_last:
19+
template = '%s NULLS LAST' % template
20+
elif self.nulls_first:
21+
template = '%s NULLS FIRST' % template
22+
else:
23+
if self.nulls_last and not (
24+
self.descending and connection.features.order_by_nulls_first
25+
) and connection.features.supports_order_by_is_nulls:
26+
template = '%%(expression)s IS NULL, %s' % template
27+
elif self.nulls_first and not (
28+
not self.descending and connection.features.order_by_nulls_first
29+
) and connection.features.supports_order_by_is_nulls:
30+
template = '%%(expression)s IS NOT NULL, %s' % template
31+
connection.ops.check_expression_support(self)
32+
expression_sql, params = compiler.compile(self.expression)
33+
placeholders = {
34+
'expression': expression_sql,
35+
'ordering': 'DESC' if self.descending else 'ASC',
36+
**extra_context,
37+
}
38+
template = template or self.template
39+
params *= template.count('%(expression)s')
40+
return (template % placeholders).rstrip(), params
41+
42+
1543
def sqlserver_atan2(self, compiler, connection, **extra_context):
1644
return self.as_sql(compiler, connection, function='ATN2', **extra_context)
1745

@@ -85,3 +113,4 @@ def sqlserver_orderby(self, compiler, connection):
85113
Exists.as_microsoft = sqlserver_exists
86114

87115
OrderBy.as_microsoft = sqlserver_orderby
116+
OrderBy.as_sql = sqlserver_as_sql

sql_server/pyodbc/operations.py

Lines changed: 64 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import datetime
22
import uuid
33
import warnings
4+
import django
45

56
from django.conf import settings
67
from django.db.backends.base.operations import BaseDatabaseOperations
8+
from django.db.models import Exists, ExpressionWrapper
9+
from django.db.models.expressions import RawSQL
10+
from django.db.models.sql.where import WhereNode
711
from django.utils import timezone
812
from django.utils.encoding import force_str
913

@@ -310,7 +314,7 @@ def savepoint_rollback_sql(self, sid):
310314
"""
311315
return "ROLLBACK TRANSACTION %s" % sid
312316

313-
def sql_flush(self, style, tables, sequences, allow_cascade=False):
317+
def sql_flush(self, style, tables, *, reset_sequences=False, allow_cascade=False):
314318
"""
315319
Returns a list of SQL statements required to remove all data from
316320
the given database tables (without actually removing the tables
@@ -325,14 +329,21 @@ def sql_flush(self, style, tables, sequences, allow_cascade=False):
325329
The `allow_cascade` argument determines whether truncation may cascade
326330
to tables with foreign keys pointing the tables being truncated.
327331
"""
328-
if tables:
329-
# Cannot use TRUNCATE on tables that are referenced by a FOREIGN KEY
330-
# So must use the much slower DELETE
331-
from django.db import connections
332-
cursor = connections[self.connection.alias].cursor()
333-
# Try to minimize the risks of the braindeaded inconsistency in
334-
# DBCC CHEKIDENT(table, RESEED, n) behavior.
335-
seqs = []
332+
if not tables:
333+
return []
334+
335+
# Cannot use TRUNCATE on tables that are referenced by a FOREIGN KEY
336+
# So must use the much slower DELETE
337+
from django.db import connections
338+
cursor = connections[self.connection.alias].cursor()
339+
# Try to minimize the risks of the braindeaded inconsistency in
340+
# DBCC CHEKIDENT(table, RESEED, n) behavior.
341+
seqs = []
342+
if reset_sequences:
343+
sequences = [
344+
sequence
345+
for sequence in self.connection.introspection.sequence_list()
346+
]
336347
for seq in sequences:
337348
cursor.execute("SELECT COUNT(*) FROM %s" % self.quote_name(seq["table"]))
338349
rowcnt = cursor.fetchone()[0]
@@ -343,37 +354,36 @@ def sql_flush(self, style, tables, sequences, allow_cascade=False):
343354
elem['start_id'] = 1
344355
elem.update(seq)
345356
seqs.append(elem)
346-
COLUMNS = "TABLE_NAME, CONSTRAINT_NAME"
347-
WHERE = "CONSTRAINT_TYPE not in ('PRIMARY KEY','UNIQUE')"
348-
cursor.execute(
349-
"SELECT {} FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE {}".format(COLUMNS, WHERE))
350-
fks = cursor.fetchall()
351-
sql_list = ['ALTER TABLE %s NOCHECK CONSTRAINT %s;' %
352-
(self.quote_name(fk[0]), self.quote_name(fk[1])) for fk in fks]
353-
sql_list.extend(['%s %s %s;' % (style.SQL_KEYWORD('DELETE'), style.SQL_KEYWORD('FROM'),
354-
style.SQL_FIELD(self.quote_name(table))) for table in tables])
355-
356-
if self.connection.to_azure_sql_db and self.connection.sql_server_version < 2014:
357-
warnings.warn("Resetting identity columns is not supported "
358-
"on this versios of Azure SQL Database.",
359-
RuntimeWarning)
360-
else:
361-
# Then reset the counters on each table.
362-
sql_list.extend(['%s %s (%s, %s, %s) %s %s;' % (
363-
style.SQL_KEYWORD('DBCC'),
364-
style.SQL_KEYWORD('CHECKIDENT'),
365-
style.SQL_FIELD(self.quote_name(seq["table"])),
366-
style.SQL_KEYWORD('RESEED'),
367-
style.SQL_FIELD('%d' % seq['start_id']),
368-
style.SQL_KEYWORD('WITH'),
369-
style.SQL_KEYWORD('NO_INFOMSGS'),
370-
) for seq in seqs])
371-
372-
sql_list.extend(['ALTER TABLE %s CHECK CONSTRAINT %s;' %
373-
(self.quote_name(fk[0]), self.quote_name(fk[1])) for fk in fks])
374-
return sql_list
357+
358+
COLUMNS = "TABLE_NAME, CONSTRAINT_NAME"
359+
WHERE = "CONSTRAINT_TYPE not in ('PRIMARY KEY','UNIQUE')"
360+
cursor.execute(
361+
"SELECT {} FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WHERE {}".format(COLUMNS, WHERE))
362+
fks = cursor.fetchall()
363+
sql_list = ['ALTER TABLE %s NOCHECK CONSTRAINT %s;' %
364+
(self.quote_name(fk[0]), self.quote_name(fk[1])) for fk in fks]
365+
sql_list.extend(['%s %s %s;' % (style.SQL_KEYWORD('DELETE'), style.SQL_KEYWORD('FROM'),
366+
style.SQL_FIELD(self.quote_name(table))) for table in tables])
367+
368+
if self.connection.to_azure_sql_db and self.connection.sql_server_version < 2014:
369+
warnings.warn("Resetting identity columns is not supported "
370+
"on this versios of Azure SQL Database.",
371+
RuntimeWarning)
375372
else:
376-
return []
373+
# Then reset the counters on each table.
374+
sql_list.extend(['%s %s (%s, %s, %s) %s %s;' % (
375+
style.SQL_KEYWORD('DBCC'),
376+
style.SQL_KEYWORD('CHECKIDENT'),
377+
style.SQL_FIELD(self.quote_name(seq["table"])),
378+
style.SQL_KEYWORD('RESEED'),
379+
style.SQL_FIELD('%d' % seq['start_id']),
380+
style.SQL_KEYWORD('WITH'),
381+
style.SQL_KEYWORD('NO_INFOMSGS'),
382+
) for seq in seqs])
383+
384+
sql_list.extend(['ALTER TABLE %s CHECK CONSTRAINT %s;' %
385+
(self.quote_name(fk[0]), self.quote_name(fk[1])) for fk in fks])
386+
return sql_list
377387

378388
def start_transaction_sql(self):
379389
"""
@@ -440,3 +450,18 @@ def time_trunc_sql(self, lookup_type, field_name):
440450
elif lookup_type == 'second':
441451
sql = "CONVERT(time, SUBSTRING(CONVERT(varchar, %s, 114), 0, 9))" % field_name
442452
return sql
453+
454+
def conditional_expression_supported_in_where_clause(self, expression):
455+
"""
456+
Following "Moved conditional expression wrapping to the Exact lookup" in django 3.1
457+
https://github.com/django/django/commit/37e6c5b79bd0529a3c85b8c478e4002fd33a2a1d
458+
"""
459+
if django.VERSION >= (3, 1):
460+
if isinstance(expression, (Exists, WhereNode)):
461+
return True
462+
if isinstance(expression, ExpressionWrapper) and expression.conditional:
463+
return self.conditional_expression_supported_in_where_clause(expression.expression)
464+
if isinstance(expression, RawSQL) and expression.conditional:
465+
return True
466+
return False
467+
return True

sql_server/pyodbc/schema.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,13 +694,15 @@ def create_unique_name(*args, **kwargs):
694694
name=name,
695695
columns=columns,
696696
condition=' WHERE ' + condition,
697+
deferrable=''
697698
) if self.connection.features.supports_partial_indexes else None
698699
else:
699700
return Statement(
700701
self.sql_create_unique,
701702
table=table,
702703
name=name,
703704
columns=columns,
705+
deferrable=''
704706
)
705707

706708
def _create_index_sql(self, model, fields, *, name=None, suffix='', using='',

tox.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
envlist =
33
{py36,py37}-django22,
44
{py36,py37,py38}-django30,
5+
{py36,py37,py38}-django31,
56

67
[testenv]
78
passenv =
@@ -19,4 +20,5 @@ commands =
1920
deps =
2021
django22: django==2.2.*
2122
django30: django>=3.0a1,<3.1
23+
django31: django>=3.1,<3.2
2224
dj-database-url==0.5.0

0 commit comments

Comments
 (0)