Skip to content

Commit c073328

Browse files
authored
Prepare for 1.4.1 release
Prepare for 1.4.1 release
2 parents c50a9bb + 9594ed5 commit c073328

File tree

14 files changed

+222
-48
lines changed

14 files changed

+222
-48
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# SQL Server backend for Django
1+
# Microsoft Django backend for SQL Server
22

33
Welcome to the MSSQL-Django 3rd party backend project!
44

azure-pipelines.yml

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,6 @@ jobs:
8282
Python 3.8 - Django 3.2:
8383
python.version: '3.8'
8484
tox.env: 'py38-django32'
85-
Python 3.7 - Django 3.2:
86-
python.version: '3.7'
87-
tox.env: 'py37-django32'
8885

8986

9087
steps:
@@ -199,9 +196,6 @@ jobs:
199196
Python 3.8 - Django 3.2:
200197
python.version: '3.8'
201198
tox.env: 'py38-django32'
202-
Python 3.7 - Django 3.2:
203-
python.version: '3.7'
204-
tox.env: 'py37-django32'
205199

206200
steps:
207201
- task: UsePythonVersion@0

mssql/base.py

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import time
1010
import struct
1111
import datetime
12+
from decimal import Decimal
13+
from uuid import UUID
1214

1315
from django.core.exceptions import ImproperlyConfigured
1416
from django.utils.functional import cached_property
@@ -124,7 +126,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
124126
'SmallIntegerField': 'smallint',
125127
'TextField': 'nvarchar(max)',
126128
'TimeField': 'time',
127-
'UUIDField': 'char(32)',
129+
'UUIDField': 'uniqueidentifier',
128130
}
129131
data_types_suffix = {
130132
'AutoField': 'IDENTITY (1, 1)',
@@ -376,7 +378,6 @@ def get_new_connection(self, conn_params):
376378
break
377379
if not need_to_retry:
378380
raise
379-
380381
# Handling values from DATETIMEOFFSET columns
381382
# source: https://github.com/mkleehammer/pyodbc/wiki/Using-an-Output-Converter-function
382383
conn.add_output_converter(SQL_TIMESTAMP_WITH_TIMEZONE, handle_datetimeoffset)
@@ -431,6 +432,9 @@ def init_connection_state(self):
431432
if (options.get('return_rows_bulk_insert', False)):
432433
self.features_class.can_return_rows_from_bulk_insert = True
433434

435+
if (options.get('has_native_uuid_field', True)):
436+
Database.native_uuid = True
437+
434438
val = self.get_system_datetime
435439
if isinstance(val, str):
436440
raise ImproperlyConfigured(
@@ -569,6 +573,36 @@ def __init__(self, cursor, connection):
569573
self.last_sql = ''
570574
self.last_params = ()
571575

576+
def _as_sql_type(self, typ, value):
577+
if isinstance(value, str):
578+
length = len(value)
579+
if length == 0:
580+
return 'NVARCHAR'
581+
elif length > 4000:
582+
return 'NVARCHAR(max)'
583+
return 'NVARCHAR(%s)' % len(value)
584+
elif typ == int:
585+
if value < 0x7FFFFFFF and value > -0x7FFFFFFF:
586+
return 'INT'
587+
else:
588+
return 'BIGINT'
589+
elif typ == float:
590+
return 'DOUBLE PRECISION'
591+
elif typ == bool:
592+
return 'BIT'
593+
elif isinstance(value, Decimal):
594+
return 'NUMERIC'
595+
elif isinstance(value, datetime.datetime):
596+
return 'DATETIME2'
597+
elif isinstance(value, datetime.date):
598+
return 'DATE'
599+
elif isinstance(value, datetime.time):
600+
return 'TIME'
601+
elif isinstance(value, UUID):
602+
return 'uniqueidentifier'
603+
else:
604+
raise NotImplementedError('Not supported type %s (%s)' % (type(value), repr(value)))
605+
572606
def close(self):
573607
if self.active:
574608
self.active = False
@@ -586,6 +620,27 @@ def format_sql(self, sql, params):
586620

587621
return sql
588622

623+
def format_group_by_params(self, query, params):
624+
if params:
625+
# Insert None params directly into the query
626+
if None in params:
627+
null_params = ['NULL' if param is None else '%s' for param in params]
628+
query = query % tuple(null_params)
629+
params = tuple(p for p in params if p is not None)
630+
params = [(param, type(param)) for param in params]
631+
params_dict = {param: '@var%d' % i for i, param in enumerate(set(params))}
632+
args = [params_dict[param] for param in params]
633+
634+
variables = []
635+
params = []
636+
for key, value in params_dict.items():
637+
datatype = self._as_sql_type(key[1], key[0])
638+
variables.append("%s %s = %%s " % (value, datatype))
639+
params.append(key[0])
640+
query = ('DECLARE %s \n' % ','.join(variables)) + (query % tuple(args))
641+
642+
return query, params
643+
589644
def format_params(self, params):
590645
fp = []
591646
if params is not None:
@@ -614,6 +669,8 @@ def format_params(self, params):
614669

615670
def execute(self, sql, params=None):
616671
self.last_sql = sql
672+
if 'GROUP BY' in sql:
673+
sql, params = self.format_group_by_params(sql, params)
617674
sql = self.format_sql(sql, params)
618675
params = self.format_params(params)
619676
self.last_params = params

mssql/compiler.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def _as_sql_window(self, compiler, connection, template=None):
155155
else:
156156
# MSSQL window functions require an OVER clause with ORDER BY
157157
window_sql.append('ORDER BY (SELECT NULL)')
158-
158+
159159
if self.frame:
160160
frame_sql, frame_params = compiler.compile(self.frame)
161161
window_sql.append(frame_sql)
@@ -443,7 +443,45 @@ def compile(self, node, *args, **kwargs):
443443

444444
def collapse_group_by(self, expressions, having):
445445
expressions = super().collapse_group_by(expressions, having)
446-
return [e for e in expressions if not isinstance(e, Subquery)]
446+
# SQL server does not allow subqueries or constant expressions in the group by
447+
# For constants: Each GROUP BY expression must contain at least one column that is not an outer reference.
448+
# For subqueries: Cannot use an aggregate or a subquery in an expression used for the group by list of a GROUP BY clause.
449+
return self._filter_subquery_and_constant_expressions(expressions)
450+
451+
def _is_constant_expression(self, expression):
452+
if isinstance(expression, Value):
453+
return True
454+
sub_exprs = expression.get_source_expressions()
455+
if not sub_exprs:
456+
return False
457+
for each in sub_exprs:
458+
if not self._is_constant_expression(each):
459+
return False
460+
return True
461+
462+
463+
464+
def _filter_subquery_and_constant_expressions(self, expressions):
465+
ret = []
466+
for expression in expressions:
467+
if self._is_subquery(expression):
468+
continue
469+
if self._is_constant_expression(expression):
470+
continue
471+
if not self._has_nested_subquery(expression):
472+
ret.append(expression)
473+
return ret
474+
475+
def _has_nested_subquery(self, expression):
476+
if self._is_subquery(expression):
477+
return True
478+
for sub_expr in expression.get_source_expressions():
479+
if self._has_nested_subquery(sub_expr):
480+
return True
481+
return False
482+
483+
def _is_subquery(self, expression):
484+
return isinstance(expression, Subquery)
447485

448486
def _as_microsoft(self, node):
449487
as_microsoft = None

mssql/features.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
1313
can_introspect_small_integer_field = True
1414
can_return_columns_from_insert = True
1515
can_return_id_from_insert = True
16-
can_return_rows_from_bulk_insert = True
16+
can_return_rows_from_bulk_insert = False
1717
can_rollback_ddl = True
1818
can_use_chunked_reads = False
1919
for_update_after_from = True
@@ -22,7 +22,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
2222
has_json_object_function = False
2323
has_json_operators = False
2424
has_native_json_field = False
25-
has_native_uuid_field = False
25+
has_native_uuid_field = True
2626
has_real_datatype = True
2727
has_select_for_update = True
2828
has_select_for_update_nowait = True
@@ -33,6 +33,7 @@ class DatabaseFeatures(BaseDatabaseFeatures):
3333
requires_literal_defaults = True
3434
requires_sqlparse_for_splitting = False
3535
supports_boolean_expr_in_select_clause = False
36+
supports_comparing_boolean_expr = False
3637
supports_comments = True
3738
supports_covering_indexes = True
3839
supports_deferrable_unique_constraints = False
@@ -60,6 +61,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
6061
supports_default_keyword_in_insert = True
6162
supports_expression_defaults = True
6263
supports_default_keyword_in_bulk_insert = True
64+
supports_stored_generated_columns = True
65+
supports_virtual_generated_columns = True
66+
6367

6468
@cached_property
6569
def has_zoneinfo_database(self):

mssql/functions.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from django.core import validators
88
from django.db import NotSupportedError, connections, transaction
99
from django.db.models import BooleanField, CheckConstraint, Value
10-
from django.db.models.expressions import Case, Exists, Expression, OrderBy, When, Window
10+
from django.db.models.expressions import Case, Exists, 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
1313
from django.db.models.functions.datetime import Now
@@ -196,8 +196,8 @@ def mssql_split_parameter_list_as_sql(self, compiler, connection):
196196
cursor.execute(f"CREATE TABLE #Temp_params (params {parameter_data_type} {Temp_table_collation})")
197197
for offset in range(0, len(rhs_params), 1000):
198198
sqls_params = rhs_params[offset: offset + 1000]
199-
sqls_params = ", ".join("('{}')".format(item) for item in sqls_params)
200-
cursor.execute("INSERT INTO #Temp_params VALUES %s" % sqls_params)
199+
sql = "INSERT INTO [#Temp_params] ([params]) VALUES " + ', '.join(['(%s)'] * len(sqls_params))
200+
cursor.execute(sql, sqls_params)
201201

202202
in_clause = lhs + ' IN ' + '(SELECT params from #Temp_params)'
203203

@@ -294,7 +294,7 @@ def _get_check_sql(self, model, schema_editor):
294294
return sql % tuple(schema_editor.quote_value(p) for p in params)
295295

296296

297-
def bulk_update_with_default(self, objs, fields, batch_size=None, default=0):
297+
def bulk_update_with_default(self, objs, fields, batch_size=None, default=None):
298298
"""
299299
Update the given fields in each of the given objects in the database.
300300
@@ -343,7 +343,8 @@ def bulk_update_with_default(self, objs, fields, batch_size=None, default=0):
343343
attr = Value(attr, output_field=field)
344344
when_statements.append(When(pk=obj.pk, then=attr))
345345
if connection.vendor == 'microsoft' and value_none_counter == len(when_statements):
346-
case_statement = Case(*when_statements, output_field=field, default=Value(default))
346+
# We don't need a case statement if we are setting everything to None
347+
case_statement = Value(None)
347348
else:
348349
case_statement = Case(*when_statements, output_field=field)
349350
if requires_casting:

mssql/operations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ def convert_floatfield_value(self, value, expression, connection):
129129

130130
def convert_uuidfield_value(self, value, expression, connection):
131131
if value is not None:
132-
value = uuid.UUID(value)
132+
value = uuid.UUID(str(value))
133133
return value
134134

135135
def convert_booleanfield_value(self, value, expression, connection):

mssql/schema.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,14 @@ def _delete_deferred_unique_indexes_for_field(self, field):
384384
def _add_deferred_unique_index_for_field(self, field, statement):
385385
self._deferred_unique_indexes[str(field)].append(statement)
386386

387+
def _column_generated_sql(self, field):
388+
"""Return the SQL to use in a GENERATED ALWAYS clause."""
389+
expression_sql, params = field.generated_sql(self.connection)
390+
persistency_sql = "PERSISTED" if field.db_persist else ""
391+
if params:
392+
expression_sql = expression_sql % tuple(self.quote_value(p) for p in params)
393+
return f"AS {expression_sql} {persistency_sql}"
394+
387395
def _alter_field(self, model, old_field, new_field, old_type, new_type,
388396
old_db_params, new_db_params, strict=False):
389397
"""Actually perform a "physical" (non-ManyToMany) field update."""
@@ -1016,6 +1024,9 @@ def add_field(self, model, field):
10161024
# It might not actually have a column behind it
10171025
if definition is None:
10181026
return
1027+
# Remove column type from definition if field is generated
1028+
if (django_version >= (5,0) and field.generated):
1029+
definition = definition[definition.find('AS'):]
10191030
# Nullable columns with default values require 'WITH VALUES' to set existing rows
10201031
if 'DEFAULT' in definition and field.null:
10211032
definition = definition.replace('NULL', 'WITH VALUES')
@@ -1218,6 +1229,9 @@ def create_model(self, model):
12181229
definition, extra_params = self.column_sql(model, field)
12191230
if definition is None:
12201231
continue
1232+
# Remove column type from definition if field is generated
1233+
if (django_version >= (5,0) and field.generated):
1234+
definition = definition[definition.find('AS'):]
12211235

12221236
if (self.connection.features.supports_nullable_unique_constraints and
12231237
not field.many_to_many and field.null and field.unique):

setup.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,11 @@
1111
"Operating System :: Microsoft :: Windows",
1212
'Programming Language :: Python',
1313
'Programming Language :: Python :: 3',
14-
'Programming Language :: Python :: 3.6',
15-
'Programming Language :: Python :: 3.7',
1614
'Programming Language :: Python :: 3.8',
1715
'Programming Language :: Python :: 3.9',
1816
'Programming Language :: Python :: 3.10',
1917
'Programming Language :: Python :: 3.11',
18+
'Programming Language :: Python :: 3.12',
2019
'Framework :: Django :: 3.2',
2120
'Framework :: Django :: 4.0',
2221
'Framework :: Django :: 4.1',
@@ -30,7 +29,7 @@
3029

3130
setup(
3231
name='mssql-django',
33-
version='1.4',
32+
version='1.4.1',
3433
description='Django backend for Microsoft SQL Server',
3534
long_description=long_description,
3635
long_description_content_type='text/markdown',
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Generated by Django 5.0.1 on 2024-01-29 14:18
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('testapp', '0024_publisher_book'),
10+
]
11+
12+
operations = [
13+
migrations.CreateModel(
14+
name='ModelWithNullableFieldsOfDifferentTypes',
15+
fields=[
16+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
17+
('int_value', models.IntegerField(null=True)),
18+
('name', models.CharField(max_length=100, null=True)),
19+
('date', models.DateTimeField(null=True)),
20+
],
21+
),
22+
]

0 commit comments

Comments
 (0)