Skip to content

Commit 1828598

Browse files
authored
Merge pull request #271 from microsoft/dev
Prepare for 1.3 release
2 parents fce7664 + c0563d3 commit 1828598

File tree

16 files changed

+274
-51
lines changed

16 files changed

+274
-51
lines changed

.github/workflows/codeql-analysis.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ jobs:
3535

3636
steps:
3737
- name: Checkout repository
38-
uses: actions/checkout@v2
38+
uses: actions/checkout@v3
3939

4040
# Initializes the CodeQL tools for scanning.
4141
- name: Initialize CodeQL
42-
uses: github/codeql-action/init@v1
42+
uses: github/codeql-action/init@v2
4343
with:
4444
languages: ${{ matrix.language }}
4545
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -50,7 +50,7 @@ jobs:
5050
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
5151
# If this step fails, then you should remove it and run the build manually (see below)
5252
- name: Autobuild
53-
uses: github/codeql-action/autobuild@v1
53+
uses: github/codeql-action/autobuild@v2
5454

5555
# ℹ️ Command-line programs to run using the OS shell.
5656
# 📚 https://git.io/JvXDl
@@ -64,4 +64,4 @@ jobs:
6464
# make release
6565

6666
- name: Perform CodeQL Analysis
67-
uses: github/codeql-action/analyze@v1
67+
uses: github/codeql-action/analyze@v2

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ We hope you enjoy using the MSSQL-Django 3rd party backend.
1010

1111
## Features
1212

13-
- Supports Django 3.2 and 4.0
14-
- Tested on Microsoft SQL Server 2016, 2017, 2019
13+
- Supports Django 3.2, 4.0, 4.1 and 4.2
14+
- Tested on Microsoft SQL Server 2016, 2017, 2019, 2022
1515
- Passes most of the tests of the Django test suite
1616
- Compatible with
1717
[Micosoft ODBC Driver for SQL Server](https://docs.microsoft.com/en-us/sql/connect/odbc/microsoft-odbc-driver-for-sql-server),

azure-pipelines.yml

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ jobs:
2424

2525
strategy:
2626
matrix:
27+
Python3.11 - Django 4.2:
28+
python.version: '3.11'
29+
tox.env: 'py311-django42'
30+
Python3.10 - Django 4.2:
31+
python.version: '3.10'
32+
tox.env: 'py310-django42'
33+
Python 3.9 - Django 4.2:
34+
python.version: '3.9'
35+
tox.env: 'py39-django42'
36+
Python 3.8 - Django 4.2:
37+
python.version: '3.8'
38+
tox.env: 'py38-django42'
39+
40+
Python3.11 - Django 4.1:
41+
python.version: '3.11'
42+
tox.env: 'py311-django41'
2743
Python3.10 - Django 4.1:
2844
python.version: '3.10'
2945
tox.env: 'py310-django41'
@@ -34,6 +50,9 @@ jobs:
3450
python.version: '3.8'
3551
tox.env: 'py38-django41'
3652

53+
Python3.11 - Django 4.0:
54+
python.version: '3.11'
55+
tox.env: 'py311-django40'
3756
Python3.10 - Django 4.0:
3857
python.version: '3.10'
3958
tox.env: 'py310-django40'
@@ -44,6 +63,9 @@ jobs:
4463
python.version: '3.8'
4564
tox.env: 'py38-django40'
4665

66+
Python3.11 - Django 3.2:
67+
python.version: '3.11'
68+
tox.env: 'py311-django32'
4769
Python 3.9 - Django 3.2:
4870
python.version: '3.9'
4971
tox.env: 'py39-django32'
@@ -53,9 +75,7 @@ jobs:
5375
Python 3.7 - Django 3.2:
5476
python.version: '3.7'
5577
tox.env: 'py37-django32'
56-
Python 3.6 - Django 3.2:
57-
python.version: '3.6'
58-
tox.env: 'py36-django32'
78+
5979

6080
steps:
6181
- task: CredScan@2
@@ -111,6 +131,22 @@ jobs:
111131

112132
strategy:
113133
matrix:
134+
Python3.11 - Django 4.2:
135+
python.version: '3.11'
136+
tox.env: 'py311-django42'
137+
Python3.10 - Django 4.2:
138+
python.version: '3.10'
139+
tox.env: 'py310-django42'
140+
Python 3.9 - Django 4.2:
141+
python.version: '3.9'
142+
tox.env: 'py39-django42'
143+
Python 3.8 - Django 4.2:
144+
python.version: '3.8'
145+
tox.env: 'py38-django42'
146+
147+
Python3.11 - Django 4.1:
148+
python.version: '3.11'
149+
tox.env: 'py311-django41'
114150
Python3.10 - Django 4.1:
115151
python.version: '3.10'
116152
tox.env: 'py310-django41'
@@ -121,6 +157,9 @@ jobs:
121157
python.version: '3.8'
122158
tox.env: 'py38-django41'
123159

160+
Python3.11 - Django 4.0:
161+
python.version: '3.11'
162+
tox.env: 'py311-django40'
124163
Python3.10 - Django 4.0:
125164
python.version: '3.10'
126165
tox.env: 'py310-django40'
@@ -131,6 +170,9 @@ jobs:
131170
python.version: '3.8'
132171
tox.env: 'py38-django40'
133172

173+
Python3.11 - Django 3.2:
174+
python.version: '3.11'
175+
tox.env: 'py311-django32'
134176
Python 3.9 - Django 3.2:
135177
python.version: '3.9'
136178
tox.env: 'py39-django32'
@@ -140,9 +182,6 @@ jobs:
140182
Python 3.7 - Django 3.2:
141183
python.version: '3.7'
142184
tox.env: 'py37-django32'
143-
Python 3.6 - Django 3.2:
144-
python.version: '3.6'
145-
tox.env: 'py36-django32'
146185

147186
steps:
148187
- task: UsePythonVersion@0

mssql/base.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import datetime
1212

1313
from django.core.exceptions import ImproperlyConfigured
14+
from django.utils.functional import cached_property
1415

1516
try:
1617
import pyodbc as Database
@@ -430,7 +431,7 @@ def init_connection_state(self):
430431
if (options.get('return_rows_bulk_insert', False)):
431432
self.features_class.can_return_rows_from_bulk_insert = True
432433

433-
val = self.get_system_datetime()
434+
val = self.get_system_datetime
434435
if isinstance(val, str):
435436
raise ImproperlyConfigured(
436437
"The database driver doesn't support modern datatime types.")
@@ -443,6 +444,7 @@ def is_usable(self):
443444
else:
444445
return True
445446

447+
@cached_property
446448
def get_system_datetime(self):
447449
# http://blogs.msdn.com/b/sqlnativeclient/archive/2008/02/27/microsoft-sql-server-native-client-and-microsoft-sql-server-2008-native-client.aspx
448450
with self.temporary_connection() as cursor:

mssql/compiler.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
from django.db.utils import NotSupportedError
1616
if django.VERSION >= (3, 1):
1717
from django.db.models.fields.json import compile_json_path, KeyTransform as json_KeyTransform
18+
if django.VERSION >= (4, 2):
19+
from django.core.exceptions import FullResultSet
1820

1921
def _as_sql_agv(self, compiler, connection):
2022
return self.as_sql(compiler, connection, template='%(function)s(CONVERT(float, %(field)s))')
@@ -76,8 +78,9 @@ def _as_sql_lpad(self, compiler, connection):
7678
params.extend(length_arg)
7779
params.extend(expression_arg)
7880
params.extend(expression_arg)
79-
template = ('LEFT(REPLICATE(%(fill_text)s, %(length)s), CASE WHEN %(length)s > LEN(%(expression)s) '
80-
'THEN %(length)s - LEN(%(expression)s) ELSE 0 END) + %(expression)s')
81+
params.extend(length_arg)
82+
template = ('LEFT(LEFT(REPLICATE(%(fill_text)s, %(length)s), CASE WHEN %(length)s > LEN(%(expression)s) '
83+
'THEN %(length)s - LEN(%(expression)s) ELSE 0 END) + %(expression)s, %(length)s)')
8184
return template % {'expression': expression, 'length': length, 'fill_text': fill_text}, params
8285

8386

@@ -227,13 +230,26 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
227230
if not getattr(features, 'supports_select_{}'.format(combinator)):
228231
raise NotSupportedError('{} is not supported on this database backend.'.format(combinator))
229232
result, params = self.get_combinator_sql(combinator, self.query.combinator_all)
233+
elif django.VERSION >= (4, 2) and self.qualify:
234+
result, params = self.get_qualify_sql()
235+
order_by = None
230236
else:
231237
distinct_fields, distinct_params = self.get_distinct()
232238
# This must come after 'select', 'ordering', and 'distinct' -- see
233239
# docstring of get_from_clause() for details.
234240
from_, f_params = self.get_from_clause()
235-
where, w_params = self.compile(self.where) if self.where is not None else ("", [])
236-
having, h_params = self.compile(self.having) if self.having is not None else ("", [])
241+
if django.VERSION >= (4, 2):
242+
try:
243+
where, w_params = self.compile(self.where) if self.where is not None else ("", [])
244+
except FullResultSet:
245+
where, w_params = "", []
246+
try:
247+
having, h_params = self.compile(self.having) if self.having is not None else ("", [])
248+
except FullResultSet:
249+
having, h_params = "", []
250+
else:
251+
where, w_params = self.compile(self.where) if self.where is not None else ("", [])
252+
having, h_params = self.compile(self.having) if self.having is not None else ("", [])
237253
params = []
238254
result = ['SELECT']
239255

@@ -364,7 +380,7 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
364380
# Django 2.x. See https://github.com/microsoft/mssql-django/issues/12
365381
# Add OFFSET for all Django versions.
366382
# https://github.com/microsoft/mssql-django/issues/109
367-
if not (do_offset or do_limit):
383+
if not (do_offset or do_limit) and supports_offset_clause:
368384
result.append("OFFSET 0 ROWS")
369385

370386
# SQL Server requires the backend-specific emulation (2008 or earlier)

mssql/features.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77

88
class DatabaseFeatures(BaseDatabaseFeatures):
9+
allows_group_by_select_index = False
910
allow_sliced_subqueries_with_in = False
1011
can_introspect_autofield = True
1112
can_introspect_json_field = False
@@ -71,4 +72,4 @@ def introspected_field_types(self):
7172
return {
7273
**super().introspected_field_types,
7374
"DurationField": "BigIntegerField",
74-
}
75+
}

mssql/functions.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
from django.db.models.expressions import Case, Exists, Expression, OrderBy, When, Window
1111
from django.db.models.fields import BinaryField, Field
1212
from django.db.models.functions import Cast, NthValue, MD5, SHA1, SHA224, SHA256, SHA384, SHA512
13+
from django.db.models.functions.datetime import Now
1314
from django.db.models.functions.math import ATan2, Ln, Log, Mod, Round, Degrees, Radians, Power
15+
from django.db.models.functions.text import Replace
1416
from django.db.models.lookups import In, Lookup
1517
from django.db.models.query import QuerySet
1618
from django.db.models.sql.query import Query
@@ -44,6 +46,19 @@ def sqlserver_log(self, compiler, connection, **extra_context):
4446
def sqlserver_ln(self, compiler, connection, **extra_context):
4547
return self.as_sql(compiler, connection, function='LOG', **extra_context)
4648

49+
50+
def sqlserver_replace(self, compiler, connection, **extra_context):
51+
current_db = "CONVERT(varchar, (SELECT DB_NAME()))"
52+
with connection.cursor() as cursor:
53+
cursor.execute("SELECT CONVERT(varchar, DATABASEPROPERTYEX(%s, 'collation'))" % current_db)
54+
default_collation = cursor.fetchone()[0]
55+
current_collation = default_collation.replace('_CI', '_CS')
56+
return self.as_sql(
57+
compiler, connection, function='REPLACE',
58+
template = 'REPLACE(%s COLLATE %s)' % ('%(expressions)s', current_collation),
59+
**extra_context
60+
)
61+
4762
def sqlserver_degrees(self, compiler, connection, **extra_context):
4863
return self.as_sql(
4964
compiler, connection, function='DEGREES',
@@ -109,6 +124,10 @@ def sqlserver_exists(self, compiler, connection, template=None, **extra_context)
109124
sql = 'CASE WHEN {} THEN 1 ELSE 0 END'.format(sql)
110125
return sql, params
111126

127+
def sqlserver_now(self, compiler, connection, **extra_context):
128+
return self.as_sql(
129+
compiler, connection, template="SYSDATETIME()", **extra_context
130+
)
112131

113132
def sqlserver_lookup(self, compiler, connection):
114133
# MSSQL doesn't allow EXISTS() to be compared to another expression
@@ -273,7 +292,7 @@ def bulk_update_with_default(self, objs, fields, batch_size=None, default=0):
273292
SQL Server require that at least one of the result expressions in a CASE specification must be an expression other than the NULL constant.
274293
Patched with a default value 0. The user can also pass a custom default value for CASE statement.
275294
"""
276-
if batch_size is not None and batch_size < 0:
295+
if batch_size is not None and batch_size <= 0:
277296
raise ValueError('Batch size must be a positive integer.')
278297
if not fields:
279298
raise ValueError('Field names must be given to bulk_update().')
@@ -441,6 +460,8 @@ def sqlserver_sha512(self, compiler, connection, **extra_context):
441460
NthValue.as_microsoft = sqlserver_nth_value
442461
Round.as_microsoft = sqlserver_round
443462
Window.as_microsoft = sqlserver_window
463+
Replace.as_microsoft = sqlserver_replace
464+
Now.as_microsoft = sqlserver_now
444465
MD5.as_microsoft = sqlserver_md5
445466
SHA1.as_microsoft = sqlserver_sha1
446467
SHA224.as_microsoft = sqlserver_sha224

mssql/introspection.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ def get_table_description(self, cursor, table_name, identity_check=True):
107107
"""
108108

109109
# map pyodbc's cursor.columns to db-api cursor description
110-
columns = [[c[3], c[4], None, c[6], c[6], c[8], c[10], c[12]] for c in cursor.columns(table=table_name)]
110+
columns = [[c[3], c[4], c[6], c[6], c[6], c[8], c[10], c[12]] for c in cursor.columns(table=table_name)]
111111

112112
if not columns:
113113
raise DatabaseError(f"Table {table_name} does not exist.")

mssql/operations.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ def limit_offset_sql(self, low_mark, high_mark):
404404
"""Return LIMIT/OFFSET SQL clause."""
405405
limit, offset = self._get_limit_offset_params(low_mark, high_mark)
406406
return '%s%s' % (
407-
(' OFFSET %d ROWS' % offset) if offset else '',
407+
(' OFFSET %d ROWS' % offset) if offset else ' OFFSET 0 ROWS',
408408
(' FETCH FIRST %d ROWS ONLY' % limit) if limit else '',
409409
)
410410

@@ -418,7 +418,13 @@ def last_executed_query(self, cursor, sql, params):
418418
exists for database backends to provide a better implementation
419419
according to their own quoting schemes.
420420
"""
421-
return super().last_executed_query(cursor, cursor.last_sql, cursor.last_params)
421+
if params:
422+
if isinstance(params, list):
423+
params = tuple(params)
424+
return sql % params
425+
# Just return sql when there are no parameters.
426+
else:
427+
return sql
422428

423429
def savepoint_create_sql(self, sid):
424430
"""

0 commit comments

Comments
 (0)