Skip to content

Commit e7c422c

Browse files
committed
support for django version 5.1
1 parent 04235d4 commit e7c422c

File tree

6 files changed

+121
-49
lines changed

6 files changed

+121
-49
lines changed

azure-pipelines.yml

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

2525
strategy:
2626
matrix:
27+
Python3.13 - Django 5.1:
28+
python.version: '3.13'
29+
tox.env: 'py313-django51'
30+
Python3.12 - Django 5.1:
31+
python.version: '3.12'
32+
tox.env: 'py312-django51'
33+
Python3.11 - Django 5.1:
34+
python.version: '3.11'
35+
tox.env: 'py311-django51'
36+
Python3.10 - Django 5.1:
37+
python.version: '3.10'
38+
tox.env: 'py310-django51'
39+
40+
Python3.12 - Django 5.0:
41+
python.version: '3.12'
42+
tox.env: 'py312-django50'
43+
Python3.11 - Django 5.0:
44+
python.version: '3.11'
45+
tox.env: 'py311-django50'
46+
Python3.10 - Django 5.0:
47+
python.version: '3.10'
48+
tox.env: 'py310-django50'
49+
2750
Python3.12 - Django 5.0:
2851
python.version: '3.12'
2952
tox.env: 'py312-django50'
@@ -138,6 +161,28 @@ jobs:
138161

139162
strategy:
140163
matrix:
164+
Python3.13 - Django 5.1:
165+
python.version: '3.13'
166+
tox.env: 'py313-django51'
167+
Python3.12 - Django 5.1:
168+
python.version: '3.12'
169+
tox.env: 'py312-django51'
170+
Python3.11 - Django 5.1:
171+
python.version: '3.11'
172+
tox.env: 'py311-django51'
173+
Python3.10 - Django 5.1:
174+
python.version: '3.10'
175+
tox.env: 'py310-django51'
176+
Python3.12 - Django 5.0:
177+
python.version: '3.12'
178+
tox.env: 'py312-django50'
179+
Python3.11 - Django 5.0:
180+
python.version: '3.11'
181+
tox.env: 'py311-django50'
182+
Python3.10 - Django 5.0:
183+
python.version: '3.10'
184+
tox.env: 'py310-django50'
185+
141186
Python3.12 - Django 5.0:
142187
python.version: '3.12'
143188
tox.env: 'py312-django50'

mssql/schema.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -292,17 +292,21 @@ def alter_unique_together(self, model, old_unique_together, new_unique_together)
292292
def _model_indexes_sql(self, model):
293293
"""
294294
Return a list of all index SQL statements (field indexes,
295-
index_together, Meta.indexes) for the specified model.
295+
Meta.indexes) for the specified model.
296296
"""
297297
if not model._meta.managed or model._meta.proxy or model._meta.swapped:
298298
return []
299299
output = []
300300
for field in model._meta.local_fields:
301301
output.extend(self._field_indexes_sql(model, field))
302-
303-
for field_names in model._meta.index_together:
304-
fields = [model._meta.get_field(field) for field in field_names]
305-
output.append(self._create_index_sql(model, fields, suffix="_idx"))
302+
# This block creates SQL for all explicit indexes defined in Meta.indexes.
303+
# Meta.indexes is the modern, recommended way to define custom indexes on model fields in Django .
304+
# It is more flexible and powerful than the legacy index_together option, which is now deprecated and removed in Django 5.1.
305+
# By using Meta.indexes, we ensure compatibility with current and future Django versions.
306+
for index in model._meta.indexes:
307+
fields = [model._meta.get_field(fname) for fname in index.fields]
308+
idx_suffix = f"_{index.name}" if index.name else "_idx"
309+
output.append(self._create_index_sql(model, fields, suffix=idx_suffix))
306310

307311
if django_version >= (4, 0):
308312
for field_names in model._meta.unique_together:
@@ -803,10 +807,15 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
803807
if old_field.db_index and new_field.db_index:
804808
index_columns.append([old_field])
805809
else:
806-
for fields in model._meta.index_together:
807-
columns = [model._meta.get_field(field) for field in fields]
808-
if old_field.column in [c.column for c in columns]:
809-
index_columns.append(columns)
810+
# This block checks all explicit indexes defined in Meta.indexes for the model.
811+
# For each index, it gathers the columns involved and checks if the old_field's column is part of that index.
812+
# If so, it adds the list of columns for that index to index_columns.
813+
# This ensures that when a field is altered or removed, any related custom indexes defined via Meta.indexes are properly handled.
814+
# This approach replaces the legacy index_together logic, ensuring compatibility with Django >=5.1
815+
for index in model._meta.indexes:
816+
columns = [model._meta.get_field(name) for name in index.fields]
817+
if old_field.column in [col.column for col in columns]:
818+
index_columns.append(columns)
810819
if index_columns:
811820
for columns in index_columns:
812821
create_index_sql_statement = self._create_index_sql(model, columns)
@@ -935,10 +944,11 @@ def _delete_indexes(self, model, old_field, new_field):
935944
index_columns.append([old_field.column])
936945
elif old_field.null != new_field.null:
937946
index_columns.append([old_field.column])
938-
for fields in model._meta.index_together:
939-
columns = [model._meta.get_field(field).column for field in fields]
940-
if old_field.column in columns:
941-
index_columns.append(columns)
947+
for index in model._meta.indexes:
948+
columns = [model._meta.get_field(field_name).column for field_name in index.fields]
949+
if old_field.column in columns:
950+
index_columns.append(columns)
951+
942952

943953
for index in model._meta.indexes:
944954
columns = [model._meta.get_field(field).column for field in index.fields]
@@ -1340,7 +1350,7 @@ def create_model(self, model):
13401350
model, field, field_type, field.db_comment
13411351
)
13421352
)
1343-
# Add any field index and index_together's (deferred as SQLite3 _remake_table needs it)
1353+
# Add any field index and Meta.indexes (deferred as SQLite3 _remake_table needs it)
13441354
self.deferred_sql.extend(self._model_indexes_sql(model))
13451355
self.deferred_sql = list(set(self.deferred_sql))
13461356

@@ -1549,4 +1559,4 @@ def _unique_supported(
15491559
nulls_distinct is None
15501560
or self.connection.features.supports_nulls_distinct_unique_constraints
15511561
)
1552-
)
1562+
)

setup.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616
'Programming Language :: Python :: 3.10',
1717
'Programming Language :: Python :: 3.11',
1818
'Programming Language :: Python :: 3.12',
19+
'Programming Language :: Python :: 3.13',
20+
1921
'Framework :: Django :: 3.2',
2022
'Framework :: Django :: 4.0',
2123
'Framework :: Django :: 4.1',
2224
'Framework :: Django :: 4.2',
2325
'Framework :: Django :: 5.0',
26+
'Framework :: Django :: 5.1',
2427
]
2528

2629
this_directory = path.abspath(path.dirname(__file__))
@@ -29,7 +32,7 @@
2932

3033
setup(
3134
name='mssql-django',
32-
version='1.5',
35+
version='1.6',
3336
description='Django backend for Microsoft SQL Server',
3437
long_description=long_description,
3538
long_description_content_type='text/markdown',
@@ -42,7 +45,7 @@
4245
license='BSD',
4346
packages=find_packages(),
4447
install_requires=[
45-
'django>=3.2,<5.1',
48+
'django>=3.2,<5.2',
4649
'pyodbc>=3.0',
4750
'pytz',
4851
],

testapp/tests/test_indexes.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ class TestCorrectIndexes(TestCase):
7979
def test_correct_indexes_exist(self):
8080
"""
8181
Check there are the correct number of indexes for each field after all migrations
82-
by comparing what the model says (e.g. `db_index=True` / `index_together` etc.)
82+
by comparing what the model says (e.g. `db_index=True` / `meta.indexes` etc.)
8383
with the actual constraints found in the database.
8484
This acts as a general regression test for issues such as:
8585
- duplicate index created (e.g. https://github.com/microsoft/mssql-django/issues/77)
@@ -116,9 +116,15 @@ def test_correct_indexes_exist(self):
116116
expected_index_causes = []
117117
if field.db_index:
118118
expected_index_causes.append('db_index=True')
119-
for field_names in model_cls._meta.index_together:
120-
if field.name in field_names:
121-
expected_index_causes.append(f'index_together[{field_names}]')
119+
# This block checks if the current field is part of any explicit Index defined in Meta.indexes.
120+
# Meta.indexes is the modern, recommended way to define custom indexes on model fields.
121+
# It is more flexible and powerful than the legacy index_together option, which is now removed.
122+
# By using Meta.indexes, we ensure compatibility with current and future Django versions.
123+
# The earlier code using index_together is replaced with this.
124+
for index in model_cls._meta.indexes:
125+
if field.name in index.fields:
126+
expected_index_causes.append(f'indexes[{tuple(index.fields)}]')
127+
122128
if field._unique and field.null:
123129
# This is implemented using a (filtered) unique index (not a constraint) to get ANSI NULL behaviour
124130
expected_index_causes.append('unique=True & null=True')

testapp/tests/test_multiple_databases.py

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@
66
from django import VERSION
77
from django.core.exceptions import ValidationError
88
from django.db import OperationalError
9-
from django.db.backends.sqlite3.operations import DatabaseOperations
9+
from django.db import connections
1010
from django.test import TestCase, skipUnlessDBFeature
11-
1211
from ..models import BinaryData, Pizza, Topping
13-
1412
if VERSION >= (3, 2):
1513
from ..models import TestCheckConstraintWithUnicode
1614

@@ -21,29 +19,35 @@
2119
)
2220
class TestMultpleDatabases(TestCase):
2321
databases = ['default', 'sqlite']
24-
25-
def test_in_split_parameter_list_as_sql(self):
26-
# Issue: https://github.com/microsoft/mssql-django/issues/92
27-
28-
# Mimic databases that have a limit on parameters (e.g. Oracle DB)
29-
old_max_in_list_size = DatabaseOperations.max_in_list_size
30-
DatabaseOperations.max_in_list_size = lambda self: 100
31-
32-
mssql_iterations = 3000
33-
Pizza.objects.bulk_create([Pizza() for _ in range(mssql_iterations)])
34-
Topping.objects.bulk_create([Topping() for _ in range(mssql_iterations)])
35-
prefetch_result = Pizza.objects.prefetch_related('toppings')
36-
self.assertEqual(len(prefetch_result), mssql_iterations)
37-
38-
# Different iterations since SQLite has max host parameters of 999 for versions prior to 3.32.0
39-
# Info about limit: https://www.sqlite.org/limits.html
40-
sqlite_iterations = 999
41-
Pizza.objects.using('sqlite').bulk_create([Pizza() for _ in range(sqlite_iterations)])
42-
Topping.objects.using('sqlite').bulk_create([Topping() for _ in range(sqlite_iterations)])
43-
prefetch_result_sqlite = Pizza.objects.using('sqlite').prefetch_related('toppings')
44-
self.assertEqual(len(prefetch_result_sqlite), sqlite_iterations)
45-
46-
DatabaseOperations.max_in_list_size = old_max_in_list_size
22+
def test_in_split_parameter_list_as_sql(self):
23+
# Issue: https://github.com/microsoft/mssql-django/issues/92
24+
25+
# Mimic databases that have a limit on parameters (e.g. Oracle DB)
26+
mssql_ops = connections['default'].ops
27+
old_max_in_list_size = mssql_ops.max_in_list_size
28+
mssql_ops.max_in_list_size = lambda: 100
29+
30+
mssql_iterations = 3000
31+
Pizza.objects.bulk_create([Pizza() for _ in range(mssql_iterations)])
32+
Topping.objects.bulk_create([Topping() for _ in range(mssql_iterations)])
33+
prefetch_result = Pizza.objects.prefetch_related('toppings')
34+
self.assertEqual(len(prefetch_result), mssql_iterations)
35+
36+
# Different iterations since SQLite has max host parameters of 999 for versions prior to 3.32.0
37+
# Info about limit: https://www.sqlite.org/limits.html
38+
sqlite_ops = connections['sqlite'].ops
39+
old_sqlite_max_in_list_size = sqlite_ops.max_in_list_size
40+
sqlite_ops.max_in_list_size = lambda: 999
41+
42+
sqlite_iterations = 999
43+
Pizza.objects.using('sqlite').bulk_create([Pizza() for _ in range(sqlite_iterations)])
44+
Topping.objects.using('sqlite').bulk_create([Topping() for _ in range(sqlite_iterations)])
45+
prefetch_result_sqlite = Pizza.objects.using('sqlite').prefetch_related('toppings')
46+
self.assertEqual(len(prefetch_result_sqlite), sqlite_iterations)
47+
48+
# Restore original methods
49+
mssql_ops.max_in_list_size = old_max_in_list_size
50+
sqlite_ops.max_in_list_size = old_sqlite_max_in_list_size
4751

4852
def test_binaryfield_init(self):
4953
binary_data = b'\x00\x46\xFE'
@@ -94,4 +98,4 @@ def test_queryset_bulk_update(self):
9498
for obj in objs:
9599
obj.binary = None
96100
BinaryData.objects.using('sqlite').bulk_update(objs, ["binary"])
97-
self.assertCountEqual(BinaryData.objects.using('sqlite').filter(binary__isnull=True), objs)
101+
self.assertCountEqual(BinaryData.objects.using('sqlite').filter(binary__isnull=True), objs)

tox.ini

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
#gave support to django version 5.1
12
[tox]
23
envlist =
34
{py36,py37,py38,py39}-django32,
45
{py38, py39, py310}-django40,
56
{py38, py39, py310}-django41,
67
{py38, py39, py310}-django42,
7-
{py310, py311, py312}-django50
8+
{py310, py311, py312}-django50,
9+
{py310, py311, py312,py313}-django51,
810

911
[testenv]
1012
allowlist_externals =
@@ -23,3 +25,5 @@ deps =
2325
django41: django>=4.1a1,<4.2
2426
django42: django>=4.2,<4.3
2527
django50: django>=5.0,<5.1
28+
django51: django>=5.1,<5.2
29+

0 commit comments

Comments
 (0)