Skip to content

Commit c50a9bb

Browse files
authored
Prepare for 1.4 release
Prepare for 1.4 release
2 parents 1828598 + d362497 commit c50a9bb

File tree

13 files changed

+405
-54
lines changed

13 files changed

+405
-54
lines changed

CodeQL.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
path_classifiers:
2+
library:
3+
- "django"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ We hope you enjoy using the MSSQL-Django 3rd party backend.
1010

1111
## Features
1212

13-
- Supports Django 3.2, 4.0, 4.1 and 4.2
13+
- Supports Django 3.2, 4.0, 4.1, 4.2 and 5.0
1414
- Tested on Microsoft SQL Server 2016, 2017, 2019, 2022
1515
- Passes most of the tests of the Django test suite
1616
- Compatible with

azure-pipelines.yml

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,21 @@ jobs:
1919
pool:
2020
name: Django-1ES-pool
2121
demands:
22-
- imageOverride -equals JDBC-MMS2019-SQL2019
22+
- imageOverride -equals JDBC-MMS2019-SQL2019-2
2323
timeoutInMinutes: 120
2424

2525
strategy:
2626
matrix:
27+
Python3.12 - Django 5.0:
28+
python.version: '3.12'
29+
tox.env: 'py312-django50'
30+
Python3.11 - Django 5.0:
31+
python.version: '3.11'
32+
tox.env: 'py311-django50'
33+
Python3.10 - Django 5.0:
34+
python.version: '3.10'
35+
tox.env: 'py310-django50'
36+
2737
Python3.11 - Django 4.2:
2838
python.version: '3.11'
2939
tox.env: 'py311-django42'
@@ -78,7 +88,7 @@ jobs:
7888

7989

8090
steps:
81-
- task: CredScan@2
91+
- task: CredScan@3
8292
inputs:
8393
toolMajorVersion: 'V2'
8494

@@ -92,7 +102,7 @@ jobs:
92102
93103
(Get-Content $pwd/testapp/settings.py).replace('localhost', $IP) | Set-Content $pwd/testapp/settings.py
94104
95-
Invoke-WebRequest https://download.microsoft.com/download/E/6/B/E6BFDC7A-5BCD-4C51-9912-635646DA801E/en-US/17.5.2.1/x64/msodbcsql.msi -OutFile msodbcsql.msi
105+
Invoke-WebRequest https://download.microsoft.com/download/6/f/f/6ffefc73-39ab-4cc0-bb7c-4093d64c2669/en-US/17.10.5.1/x64/msodbcsql.msi -OutFile msodbcsql.msi
96106
msiexec /quiet /passive /qn /i msodbcsql.msi IACCEPTMSODBCSQLLICENSETERMS=YES
97107
Get-OdbcDriver
98108
displayName: Install ODBC
@@ -126,11 +136,21 @@ jobs:
126136
pool:
127137
name: Django-1ES-pool
128138
demands:
129-
- imageOverride -equals MMSUbuntu20.04
139+
- imageOverride -equals Ubuntu22.04-AzurePipelines
130140
timeoutInMinutes: 120
131141

132142
strategy:
133143
matrix:
144+
Python3.12 - Django 5.0:
145+
python.version: '3.12'
146+
tox.env: 'py312-django50'
147+
Python3.11 - Django 5.0:
148+
python.version: '3.11'
149+
tox.env: 'py311-django50'
150+
Python3.10 - Django 5.0:
151+
python.version: '3.10'
152+
tox.env: 'py310-django50'
153+
134154
Python3.11 - Django 4.2:
135155
python.version: '3.11'
136156
tox.env: 'py311-django42'
@@ -190,12 +210,11 @@ jobs:
190210
displayName: Use Python $(python.version)
191211

192212
- script: |
213+
docker version
193214
docker pull mcr.microsoft.com/mssql/server:2022-latest
194215
docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=$(TestAppPassword)' -p 1433:1433 -d mcr.microsoft.com/mssql/server:2022-latest
195216
curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
196-
curl https://packages.microsoft.com/config/ubuntu/20.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
197-
sudo apt-get update
198-
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql17 g++ unixodbc-dev libmemcached-dev
217+
curl https://packages.microsoft.com/config/ubuntu/22.04/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
199218
displayName: Install SQL Server
200219
201220
- script: |

mssql/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ def format_sql(self, sql, params):
581581
sql = smart_str(sql, self.driver_charset)
582582

583583
# pyodbc uses '?' instead of '%s' as parameter placeholder.
584-
if params is not None:
584+
if params is not None and params != []:
585585
sql = sql % tuple('?' * len(params))
586586

587587
return sql

mssql/compiler.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
if django.VERSION >= (3, 1):
1717
from django.db.models.fields.json import compile_json_path, KeyTransform as json_KeyTransform
1818
if django.VERSION >= (4, 2):
19-
from django.core.exceptions import FullResultSet
19+
from django.core.exceptions import EmptyResultSet, FullResultSet
2020

2121
def _as_sql_agv(self, compiler, connection):
2222
return self.as_sql(compiler, connection, template='%(function)s(CONVERT(float, %(field)s))')
@@ -241,6 +241,11 @@ def as_sql(self, with_limits=True, with_col_aliases=False):
241241
if django.VERSION >= (4, 2):
242242
try:
243243
where, w_params = self.compile(self.where) if self.where is not None else ("", [])
244+
except EmptyResultSet:
245+
if self.elide_empty:
246+
raise
247+
# Use a predicate that's always False.
248+
where, w_params = "0 = 1", []
244249
except FullResultSet:
245250
where, w_params = "", []
246251
try:

mssql/features.py

Lines changed: 5 additions & 1 deletion
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 = False
16+
can_return_rows_from_bulk_insert = True
1717
can_rollback_ddl = True
1818
can_use_chunked_reads = False
1919
for_update_after_from = 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_comments = True
3637
supports_covering_indexes = True
3738
supports_deferrable_unique_constraints = False
3839
supports_expression_indexes = False
@@ -56,6 +57,9 @@ class DatabaseFeatures(BaseDatabaseFeatures):
5657
supports_partially_nullable_unique_constraints = True
5758
supports_partial_indexes = True
5859
supports_functions_in_partial_indexes = True
60+
supports_default_keyword_in_insert = True
61+
supports_expression_defaults = True
62+
supports_default_keyword_in_bulk_insert = True
5963

6064
@cached_property
6165
def has_zoneinfo_database(self):

mssql/functions.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,16 @@
3232
class TryCast(Cast):
3333
function = 'TRY_CAST'
3434

35+
def sqlserver_cast(self, compiler, connection, **extra_context):
36+
if hasattr(self.source_expressions[0], 'lookup_name'):
37+
if self.source_expressions[0].lookup_name in ['gt', 'gte', 'lt', 'lte']:
38+
return self.as_sql(
39+
compiler, connection,
40+
template = 'CASE WHEN %(expressions)s THEN 1 ELSE 0 END',
41+
**extra_context
42+
)
43+
return self.as_sql(compiler, connection, **extra_context)
44+
3545

3646
def sqlserver_atan2(self, compiler, connection, **extra_context):
3747
return self.as_sql(compiler, connection, function='ATN2', **extra_context)
@@ -451,6 +461,7 @@ def sqlserver_sha512(self, compiler, connection, **extra_context):
451461
key_transform_exact_process_rhs = KeyTransformExact.process_rhs
452462
KeyTransformExact.process_rhs = json_KeyTransformExact_process_rhs
453463
HasKeyLookup.as_microsoft = json_HasKeyLookup
464+
Cast.as_microsoft = sqlserver_cast
454465
Degrees.as_microsoft = sqlserver_degrees
455466
Radians.as_microsoft = sqlserver_radians
456467
Power.as_microsoft = sqlserver_power

mssql/introspection.py

Lines changed: 82 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@
44
from django.db import DatabaseError
55
import pyodbc as Database
66

7+
from collections import namedtuple
8+
79
from django import VERSION
8-
from django.db.backends.base.introspection import (
9-
BaseDatabaseIntrospection, FieldInfo, TableInfo,
10-
)
10+
from django.db.backends.base.introspection import BaseDatabaseIntrospection
11+
from django.db.backends.base.introspection import FieldInfo as BaseFieldInfo
12+
from django.db.backends.base.introspection import TableInfo as BaseTableInfo
1113
from django.db.models.indexes import Index
1214
from django.conf import settings
1315

1416
SQL_AUTOFIELD = -777555
1517
SQL_BIGAUTOFIELD = -777444
18+
SQL_SMALLAUTOFIELD = -777333
1619
SQL_TIMESTAMP_WITH_TIMEZONE = -155
1720

21+
FieldInfo = namedtuple("FieldInfo", BaseFieldInfo._fields + ("comment",))
22+
TableInfo = namedtuple("TableInfo", BaseTableInfo._fields + ("comment",))
1823

1924
def get_schema_name():
2025
return getattr(settings, 'SCHEMA_TO_INSPECT', 'SCHEMA_NAME()')
@@ -25,6 +30,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
2530
data_types_reverse = {
2631
SQL_AUTOFIELD: 'AutoField',
2732
SQL_BIGAUTOFIELD: 'BigAutoField',
33+
SQL_SMALLAUTOFIELD: 'SmallAutoField',
2834
Database.SQL_BIGINT: 'BigIntegerField',
2935
# Database.SQL_BINARY: ,
3036
Database.SQL_BIT: 'BooleanField',
@@ -71,13 +77,26 @@ def get_table_list(self, cursor):
7177
"""
7278
Returns a list of table and view names in the current database.
7379
"""
74-
sql = 'SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = %s' % (
80+
sql = """SELECT
81+
TABLE_NAME,
82+
TABLE_TYPE,
83+
CAST(ep.value AS VARCHAR) AS COMMENT
84+
FROM INFORMATION_SCHEMA.TABLES i
85+
LEFT JOIN sys.tables t ON t.name = i.TABLE_NAME
86+
LEFT JOIN sys.extended_properties ep ON t.object_id = ep.major_id
87+
AND ((ep.name = 'MS_DESCRIPTION' AND ep.minor_id = 0) OR ep.value IS NULL)
88+
AND i.TABLE_SCHEMA = %s""" % (
7589
get_schema_name())
7690
cursor.execute(sql)
7791
types = {'BASE TABLE': 't', 'VIEW': 'v'}
78-
return [TableInfo(row[0], types.get(row[1]))
79-
for row in cursor.fetchall()
80-
if row[0] not in self.ignored_tables]
92+
if VERSION >= (4, 2):
93+
return [TableInfo(row[0], types.get(row[1]), row[2])
94+
for row in cursor.fetchall()
95+
if row[0] not in self.ignored_tables]
96+
else:
97+
return [BaseTableInfo(row[0], types.get(row[1]))
98+
for row in cursor.fetchall()
99+
if row[0] not in self.ignored_tables]
81100

82101
def _is_auto_field(self, cursor, table_name, column_name):
83102
"""
@@ -111,7 +130,7 @@ def get_table_description(self, cursor, table_name, identity_check=True):
111130

112131
if not columns:
113132
raise DatabaseError(f"Table {table_name} does not exist.")
114-
133+
115134
items = []
116135
for column in columns:
117136
if VERSION >= (3, 2):
@@ -126,15 +145,40 @@ def get_table_description(self, cursor, table_name, identity_check=True):
126145
column.append(collation_name[0] if collation_name else '')
127146
else:
128147
column.append('')
129-
148+
if VERSION >= (4, 2):
149+
sql = """select CAST(ep.value AS VARCHAR) AS COMMENT
150+
FROM sys.columns c
151+
INNER JOIN sys.tables t ON c.object_id = t.object_id
152+
INNER JOIN sys.extended_properties ep ON c.object_id=ep.major_id AND ep.minor_id = c.column_id
153+
WHERE t.name = '%s' AND c.name = '%s' AND ep.name = 'MS_Description'
154+
""" % (table_name, column[0])
155+
cursor.execute(sql)
156+
comment = cursor.fetchone()
157+
column.append(comment[0] if comment else '')
130158
if identity_check and self._is_auto_field(cursor, table_name, column[0]):
131159
if column[1] == Database.SQL_BIGINT:
132160
column[1] = SQL_BIGAUTOFIELD
161+
elif column[1] == Database.SQL_SMALLINT:
162+
column[1] = SQL_SMALLAUTOFIELD
133163
else:
134164
column[1] = SQL_AUTOFIELD
135165
if column[1] == Database.SQL_WVARCHAR and column[3] < 4000:
136166
column[1] = Database.SQL_WCHAR
137-
items.append(FieldInfo(*column))
167+
# Remove surrounding parentheses for default values
168+
if column[7]:
169+
default_value = column[7]
170+
start = 0
171+
end = -1
172+
for _ in range(2):
173+
if default_value[start] == '(' and default_value[end] == ')':
174+
start += 1
175+
end -= 1
176+
column[7] = default_value[start:end + 1]
177+
178+
if VERSION >= (4, 2):
179+
items.append(FieldInfo(*column))
180+
else:
181+
items.append(BaseFieldInfo(*column))
138182
return items
139183

140184
def get_sequences(self, cursor, table_name, table_fields=()):
@@ -271,6 +315,7 @@ def get_constraints(self, cursor, table_name):
271315
# Potentially misleading: primary key and unique constraints still have indexes attached to them.
272316
# Should probably be updated with the additional info from the sys.indexes table we fetch later on.
273317
"index": False,
318+
"default": False,
274319
}
275320
# Record the details
276321
constraints[constraint]['columns'].append(column)
@@ -298,6 +343,32 @@ def get_constraints(self, cursor, table_name):
298343
"foreign_key": None,
299344
"check": True,
300345
"index": False,
346+
"default": False,
347+
}
348+
# Record the details
349+
constraints[constraint]['columns'].append(column)
350+
# Now get DEFAULT constraint columns
351+
cursor.execute("""
352+
SELECT
353+
[name],
354+
COL_NAME([parent_object_id], [parent_column_id])
355+
FROM
356+
[sys].[default_constraints]
357+
WHERE
358+
OBJECT_NAME([parent_object_id]) = %s
359+
""", [table_name])
360+
for constraint, column in cursor.fetchall():
361+
# If we're the first column, make the record
362+
if constraint not in constraints:
363+
constraints[constraint] = {
364+
"columns": [],
365+
"primary_key": False,
366+
"unique": False,
367+
"unique_constraint": False,
368+
"foreign_key": None,
369+
"check": False,
370+
"index": False,
371+
"default": True,
301372
}
302373
# Record the details
303374
constraints[constraint]['columns'].append(column)
@@ -341,6 +412,7 @@ def get_constraints(self, cursor, table_name):
341412
"unique_constraint": unique_constraint,
342413
"foreign_key": None,
343414
"check": False,
415+
"default": False,
344416
"index": True,
345417
"orders": [],
346418
"type": Index.suffix if type_ in (1, 2) else desc.lower(),

mssql/operations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def date_trunc_sql(self, lookup_type, sql, params, tzname=None):
178178

179179
# Python formats year with leading zeroes. This preserves that format for
180180
# compatibility with SQL Server's date since DATEPART drops the leading zeroes.
181-
CONVERT_YEAR = 'CONVERT(varchar(4), %s)' % sql
181+
CONVERT_YEAR = 'CONVERT(varchar(4), CONVERT(date, %s))' % sql
182182
CONVERT_QUARTER = 'CONVERT(varchar, 1+((DATEPART(quarter, %s)-1)*3))' % sql
183183
CONVERT_MONTH = 'CONVERT(varchar, DATEPART(month, %s))' % sql
184184
CONVERT_WEEK = "DATEADD(DAY, (DATEPART(weekday, %s) + 5) %%%% 7 * -1, %s)" % (sql, sql)

0 commit comments

Comments
 (0)