Skip to content

Commit c0c456f

Browse files
mShan0jmah8maikhanhbui
authored
add support for django 4.1 (#208)
* Fixed failing timezone unit test for Django 4.1 (#169) * Added Django 4.1 tests into test suite * Fixed datetime issues * Added skipped timezone tests * fix date_trunc_sql to work with < 4-digit year set has_case_insensitive_like to True Revert "fix date_trunc_sql to work with < 4-digit year" This reverts commit 492e32c. Revert "set has_case_insensitive_like to True" This reverts commit 2bc4f8e. fix date_trunc_sql to work with < 4-digit year (#188) * set has_case_insensitive_like to True (#189) Co-Authored-By: Khanh Bui <85855766+khanhmaibui@users.noreply.github.com> Co-authored-by: Khanh Bui <85855766+khanhmaibui@users.noreply.github.com> * bump required django version to include 4.1.x updates (#192) * add introspected field type for DurationField (#191) * Add check to see if `FROM` clause exists (#190) * Make MSSQL introspection raise DatabaseError on nonexistent tables (#194) * Make MSSQL introspection raise DatabaseError on nonexistent tables * edit error message to make it specific to MSSQL * Refix JSONField has_key so it doesn't break previous Django versions (#195) * Fix removing unique_together constraint if exists primary key/unique constraint on the same field. (#204) * Fix removing unique_together constraint if exists primary key/unique constraint on the same field. * add django version requirement * fix jsonfield test in sql server 2022 and newer (#203) * fix jsonfield test in sql server 2022 * fix sql server version * fix bulk update tests (#205) * match native function updates * syntax update * fix `mssql-django` tests * trim whitespace * create custom `Window.as_sql` (#207) Co-Authored-By: Khanh Bui 85855766+khanhmaibui@users.noreply.github.com * Add skipped tests to Django 4.1 (#199) Skips the currently unfixable tests added by Django 4.1 Co-authored-by: mShan0 <mark.shan19@gmail.com> Co-authored-by: jmah8 <59151084+jmah8@users.noreply.github.com> Co-authored-by: Khanh Bui <khanhmb815@gmail.com> Co-authored-by: Khanh Bui <85855766+khanhmaibui@users.noreply.github.com>
1 parent b455faf commit c0c456f

File tree

10 files changed

+306
-87
lines changed

10 files changed

+306
-87
lines changed

azure-pipelines.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,16 @@ jobs:
2424

2525
strategy:
2626
matrix:
27+
Python3.10 - Django 4.1:
28+
python.version: '3.10'
29+
tox.env: 'py310-django41'
30+
Python 3.9 - Django 4.1:
31+
python.version: '3.9'
32+
tox.env: 'py39-django41'
33+
Python 3.8 - Django 4.1:
34+
python.version: '3.8'
35+
tox.env: 'py38-django41'
36+
2737
Python3.10 - Django 4.0:
2838
python.version: '3.10'
2939
tox.env: 'py310-django40'
@@ -101,6 +111,16 @@ jobs:
101111

102112
strategy:
103113
matrix:
114+
Python3.10 - Django 4.1:
115+
python.version: '3.10'
116+
tox.env: 'py310-django41'
117+
Python 3.9 - Django 4.1:
118+
python.version: '3.9'
119+
tox.env: 'py39-django41'
120+
Python 3.8 - Django 4.1:
121+
python.version: '3.8'
122+
tox.env: 'py38-django41'
123+
104124
Python3.10 - Django 4.0:
105125
python.version: '3.10'
106126
tox.env: 'py310-django40'

mssql/compiler.py

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

77
import django
88
from django.db.models.aggregates import Avg, Count, StdDev, Variance
9-
from django.db.models.expressions import Ref, Subquery, Value
9+
from django.db.models.expressions import Ref, Subquery, Value, Window
1010
from django.db.models.functions import (
1111
Chr, ConcatPair, Greatest, Least, Length, LPad, Random, Repeat, RPad, StrIndex, Substr, Trim
1212
)
@@ -129,6 +129,41 @@ def _as_sql_variance(self, compiler, connection):
129129
function = '%sP' % function
130130
return self.as_sql(compiler, connection, function=function)
131131

132+
def _as_sql_window(self, compiler, connection, template=None):
133+
connection.ops.check_expression_support(self)
134+
if not connection.features.supports_over_clause:
135+
raise NotSupportedError("This backend does not support window expressions.")
136+
expr_sql, params = compiler.compile(self.source_expression)
137+
window_sql, window_params = [], ()
138+
139+
if self.partition_by is not None:
140+
sql_expr, sql_params = self.partition_by.as_sql(
141+
compiler=compiler,
142+
connection=connection,
143+
template="PARTITION BY %(expressions)s",
144+
)
145+
window_sql.append(sql_expr)
146+
window_params += tuple(sql_params)
147+
148+
if self.order_by is not None:
149+
order_sql, order_params = compiler.compile(self.order_by)
150+
window_sql.append(order_sql)
151+
window_params += tuple(order_params)
152+
else:
153+
# MSSQL window functions require an OVER clause with ORDER BY
154+
window_sql.append('ORDER BY (SELECT NULL)')
155+
156+
if self.frame:
157+
frame_sql, frame_params = compiler.compile(self.frame)
158+
window_sql.append(frame_sql)
159+
window_params += tuple(frame_params)
160+
161+
template = template or self.template
162+
163+
return (
164+
template % {"expression": expr_sql, "window": " ".join(window_sql).strip()},
165+
(*params, *window_params),
166+
)
132167

133168
def _cursor_iter(cursor, sentinel, col_count, itersize):
134169
"""
@@ -281,7 +316,9 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
281316
if for_update_part and self.connection.features.for_update_after_from:
282317
from_.insert(1, for_update_part)
283318

284-
result += [', '.join(out_cols), 'FROM', *from_]
319+
result += [', '.join(out_cols)]
320+
if from_:
321+
result += ['FROM', *from_]
285322
params.extend(f_params)
286323

287324
if where:
@@ -422,6 +459,9 @@ def _as_microsoft(self, node):
422459
if django.VERSION >= (3, 1):
423460
if isinstance(node, json_KeyTransform):
424461
as_microsoft = _as_sql_json_keytransform
462+
if django.VERSION >= (4, 1):
463+
if isinstance(node, Window):
464+
as_microsoft = _as_sql_window
425465
if as_microsoft:
426466
node = node.copy()
427467
node.as_microsoft = types.MethodType(as_microsoft, node)

mssql/features.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
1717
can_use_chunked_reads = False
1818
for_update_after_from = True
1919
greatest_least_ignores_nulls = True
20+
has_case_insensitive_like = True
2021
has_json_object_function = False
2122
has_json_operators = False
2223
has_native_json_field = False
@@ -64,3 +65,10 @@ def has_zoneinfo_database(self):
6465
@cached_property
6566
def supports_json_field(self):
6667
return self.connection.sql_server_version >= 2016 or self.connection.to_azure_sql_db
68+
69+
@cached_property
70+
def introspected_field_types(self):
71+
return {
72+
**super().introspected_field_types,
73+
"DurationField": "BigIntegerField",
74+
}

mssql/functions.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from django.db.models.functions.math import Random
2525

2626
DJANGO3 = VERSION[0] >= 3
27+
DJANGO41 = VERSION >= (4, 1)
2728

2829

2930
class TryCast(Cast):
@@ -95,7 +96,7 @@ def sqlserver_random(self, compiler, connection, **extra_context):
9596

9697
def sqlserver_window(self, compiler, connection, template=None):
9798
# MSSQL window functions require an OVER clause with ORDER BY
98-
if self.order_by is None:
99+
if VERSION < (4, 1) and self.order_by is None:
99100
self.order_by = Value('SELECT NULL')
100101
return self.as_sql(compiler, connection, template)
101102

@@ -204,8 +205,11 @@ def json_HasKeyLookup(self, compiler, connection):
204205
else:
205206
lhs, _ = self.process_lhs(compiler, connection)
206207
lhs_json_path = '$'
207-
sql = lhs + ' IN (SELECT ' + lhs + ' FROM ' + self.lhs.output_field.model._meta.db_table + \
208-
' CROSS APPLY OPENJSON(' + lhs + ') WITH ( [json_path_value] char(1) \'%s\') WHERE [json_path_value] IS NOT NULL)'
208+
if connection.sql_server_version >= 2022:
209+
sql = "JSON_PATH_EXISTS(%s, '%%s') > 0" % lhs
210+
else:
211+
sql = lhs + ' IN (SELECT ' + lhs + ' FROM ' + self.lhs.output_field.model._meta.db_table + \
212+
' CROSS APPLY OPENJSON(' + lhs + ') WITH ( [json_path_value] char(1) \'%s\') WHERE [json_path_value] IS NOT NULL)'
209213
# Process JSON path from the right-hand side.
210214
rhs = self.rhs
211215
rhs_params = []
@@ -216,7 +220,13 @@ def json_HasKeyLookup(self, compiler, connection):
216220
*_, rhs_key_transforms = key.preprocess_lhs(compiler, connection)
217221
else:
218222
rhs_key_transforms = [key]
219-
rhs_params.append('%s%s' % (
223+
if VERSION >= (4, 1):
224+
*rhs_key_transforms, final_key = rhs_key_transforms
225+
rhs_json_path = compile_json_path(rhs_key_transforms, include_root=False)
226+
rhs_json_path += self.compile_json_path_final_key(final_key)
227+
rhs_params.append(lhs_json_path + rhs_json_path)
228+
else:
229+
rhs_params.append('%s%s' % (
220230
lhs_json_path,
221231
compile_json_path(rhs_key_transforms, include_root=False),
222232
))
@@ -277,11 +287,18 @@ def bulk_update_with_default(self, objs, fields, batch_size=None, default=0):
277287
raise ValueError('bulk_update() cannot be used with primary key fields.')
278288
if not objs:
279289
return 0
290+
if DJANGO41:
291+
for obj in objs:
292+
obj._prepare_related_fields_for_save(
293+
operation_name="bulk_update", fields=fields
294+
)
280295
# PK is used twice in the resulting update query, once in the filter
281296
# and once in the WHEN. Each field will also have one CAST.
282-
max_batch_size = connections[self.db].ops.bulk_batch_size(['pk', 'pk'] + fields, objs)
297+
self._for_write = True
298+
connection = connections[self.db]
299+
max_batch_size = connection.ops.bulk_batch_size(['pk', 'pk'] + fields, objs)
283300
batch_size = min(batch_size, max_batch_size) if batch_size else max_batch_size
284-
requires_casting = connections[self.db].features.requires_casted_case_in_updates
301+
requires_casting = connection.features.requires_casted_case_in_updates
285302
batches = (objs[i:i + batch_size] for i in range(0, len(objs), batch_size))
286303
updates = []
287304
for batch_objs in batches:
@@ -291,12 +308,12 @@ def bulk_update_with_default(self, objs, fields, batch_size=None, default=0):
291308
when_statements = []
292309
for obj in batch_objs:
293310
attr = getattr(obj, field.attname)
294-
if not isinstance(attr, Expression):
311+
if not hasattr(attr, "resolve_expression"):
295312
if attr is None:
296313
value_none_counter += 1
297314
attr = Value(attr, output_field=field)
298315
when_statements.append(When(pk=obj.pk, then=attr))
299-
if connections[self.db].vendor == 'microsoft' and value_none_counter == len(when_statements):
316+
if connection.vendor == 'microsoft' and value_none_counter == len(when_statements):
300317
case_statement = Case(*when_statements, output_field=field, default=Value(default))
301318
else:
302319
case_statement = Case(*when_statements, output_field=field)
@@ -305,9 +322,10 @@ def bulk_update_with_default(self, objs, fields, batch_size=None, default=0):
305322
update_kwargs[field.attname] = case_statement
306323
updates.append(([obj.pk for obj in batch_objs], update_kwargs))
307324
rows_updated = 0
325+
queryset = self.using(self.db)
308326
with transaction.atomic(using=self.db, savepoint=False):
309327
for pks, update_kwargs in updates:
310-
rows_updated += self.filter(pk__in=pks).update(**update_kwargs)
328+
rows_updated += queryset.filter(pk__in=pks).update(**update_kwargs)
311329
return rows_updated
312330

313331

mssql/introspection.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Copyright (c) Microsoft Corporation.
22
# Licensed under the BSD license.
33

4+
from django.db import DatabaseError
45
import pyodbc as Database
56

67
from django import VERSION
@@ -108,6 +109,9 @@ def get_table_description(self, cursor, table_name, identity_check=True):
108109
# map pyodbc's cursor.columns to db-api cursor description
109110
columns = [[c[3], c[4], None, c[6], c[6], c[8], c[10], c[12]] for c in cursor.columns(table=table_name)]
110111

112+
if not columns:
113+
raise DatabaseError(f"Table {table_name} does not exist.")
114+
111115
items = []
112116
for column in columns:
113117
if VERSION >= (3, 2):

0 commit comments

Comments
 (0)