Skip to content

FEAT: Added support to version 5.1 #442

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,29 @@ jobs:

strategy:
matrix:
Python3.13 - Django 5.1:
python.version: '3.13'
tox.env: 'py313-django51'
Python3.12 - Django 5.1:
python.version: '3.12'
tox.env: 'py312-django51'
Python3.11 - Django 5.1:
python.version: '3.11'
tox.env: 'py311-django51'
Python3.10 - Django 5.1:
python.version: '3.10'
tox.env: 'py310-django51'

Python3.12 - Django 5.0:
python.version: '3.12'
tox.env: 'py312-django50'
Python3.11 - Django 5.0:
python.version: '3.11'
tox.env: 'py311-django50'
Python3.10 - Django 5.0:
python.version: '3.10'
tox.env: 'py310-django50'

Python3.12 - Django 5.0:
python.version: '3.12'
tox.env: 'py312-django50'
Expand Down Expand Up @@ -138,6 +161,28 @@ jobs:

strategy:
matrix:
Python3.13 - Django 5.1:
python.version: '3.13'
tox.env: 'py313-django51'
Python3.12 - Django 5.1:
python.version: '3.12'
tox.env: 'py312-django51'
Python3.11 - Django 5.1:
python.version: '3.11'
tox.env: 'py311-django51'
Python3.10 - Django 5.1:
python.version: '3.10'
tox.env: 'py310-django51'
Python3.12 - Django 5.0:
python.version: '3.12'
tox.env: 'py312-django50'
Python3.11 - Django 5.0:
python.version: '3.11'
tox.env: 'py311-django50'
Python3.10 - Django 5.0:
python.version: '3.10'
tox.env: 'py310-django50'

Python3.12 - Django 5.0:
python.version: '3.12'
tox.env: 'py312-django50'
Expand Down Expand Up @@ -235,3 +280,4 @@ jobs:
testResultsFormat: 'JUnit'
testResultsFiles: 'django/result.xml'
testRunTitle: 'junit-$(Agent.OS)-$(Agent.OSArchitecture)-$(tox.env)'

40 changes: 25 additions & 15 deletions mssql/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,17 +292,21 @@ def alter_unique_together(self, model, old_unique_together, new_unique_together)
def _model_indexes_sql(self, model):
"""
Return a list of all index SQL statements (field indexes,
index_together, Meta.indexes) for the specified model.
Meta.indexes) for the specified model.
"""
if not model._meta.managed or model._meta.proxy or model._meta.swapped:
return []
output = []
for field in model._meta.local_fields:
output.extend(self._field_indexes_sql(model, field))

for field_names in model._meta.index_together:
fields = [model._meta.get_field(field) for field in field_names]
output.append(self._create_index_sql(model, fields, suffix="_idx"))
# This block creates SQL for all explicit indexes defined in Meta.indexes.
# Meta.indexes is the modern, recommended way to define custom indexes on model fields in Django .
# It is more flexible and powerful than the legacy index_together option, which is now deprecated and removed in Django 5.1.
# By using Meta.indexes, we ensure compatibility with current and future Django versions.
for index in model._meta.indexes:
fields = [model._meta.get_field(fname) for fname in index.fields]
idx_suffix = f"_{index.name}" if index.name else "_idx"
output.append(self._create_index_sql(model, fields, suffix=idx_suffix))

if django_version >= (4, 0):
for field_names in model._meta.unique_together:
Expand Down Expand Up @@ -803,10 +807,15 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
if old_field.db_index and new_field.db_index:
index_columns.append([old_field])
else:
for fields in model._meta.index_together:
columns = [model._meta.get_field(field) for field in fields]
if old_field.column in [c.column for c in columns]:
index_columns.append(columns)
# This block checks all explicit indexes defined in Meta.indexes for the model.
# For each index, it gathers the columns involved and checks if the old_field's column is part of that index.
# If so, it adds the list of columns for that index to index_columns.
# This ensures that when a field is altered or removed, any related custom indexes defined via Meta.indexes are properly handled.
# This approach replaces the legacy index_together logic, ensuring compatibility with Django >=5.1
for index in model._meta.indexes:
columns = [model._meta.get_field(name) for name in index.fields]
if old_field.column in [col.column for col in columns]:
index_columns.append(columns)
if index_columns:
for columns in index_columns:
create_index_sql_statement = self._create_index_sql(model, columns)
Expand Down Expand Up @@ -935,10 +944,11 @@ def _delete_indexes(self, model, old_field, new_field):
index_columns.append([old_field.column])
elif old_field.null != new_field.null:
index_columns.append([old_field.column])
for fields in model._meta.index_together:
columns = [model._meta.get_field(field).column for field in fields]
if old_field.column in columns:
index_columns.append(columns)
for index in model._meta.indexes:
columns = [model._meta.get_field(field_name).column for field_name in index.fields]
if old_field.column in columns:
index_columns.append(columns)


for index in model._meta.indexes:
columns = [model._meta.get_field(field).column for field in index.fields]
Expand Down Expand Up @@ -1340,7 +1350,7 @@ def create_model(self, model):
model, field, field_type, field.db_comment
)
)
# Add any field index and index_together's (deferred as SQLite3 _remake_table needs it)
# Add any field index and Meta.indexes (deferred as SQLite3 _remake_table needs it)
self.deferred_sql.extend(self._model_indexes_sql(model))
self.deferred_sql = list(set(self.deferred_sql))

Expand Down Expand Up @@ -1549,4 +1559,4 @@ def _unique_supported(
nulls_distinct is None
or self.connection.features.supports_nulls_distinct_unique_constraints
)
)
)
4 changes: 3 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13',
'Framework :: Django :: 3.2',
'Framework :: Django :: 4.0',
'Framework :: Django :: 4.1',
'Framework :: Django :: 4.2',
'Framework :: Django :: 5.0',
'Framework :: Django :: 5.1',
]

this_directory = path.abspath(path.dirname(__file__))
Expand All @@ -42,7 +44,7 @@
license='BSD',
packages=find_packages(),
install_requires=[
'django>=3.2,<5.1',
'django>=3.2,<5.2',
'pyodbc>=3.0',
'pytz',
],
Expand Down
14 changes: 10 additions & 4 deletions testapp/tests/test_indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class TestCorrectIndexes(TestCase):
def test_correct_indexes_exist(self):
"""
Check there are the correct number of indexes for each field after all migrations
by comparing what the model says (e.g. `db_index=True` / `index_together` etc.)
by comparing what the model says (e.g. `db_index=True` / `meta.indexes` etc.)
with the actual constraints found in the database.
This acts as a general regression test for issues such as:
- duplicate index created (e.g. https://github.com/microsoft/mssql-django/issues/77)
Expand Down Expand Up @@ -116,9 +116,15 @@ def test_correct_indexes_exist(self):
expected_index_causes = []
if field.db_index:
expected_index_causes.append('db_index=True')
for field_names in model_cls._meta.index_together:
if field.name in field_names:
expected_index_causes.append(f'index_together[{field_names}]')
# This block checks if the current field is part of any explicit Index defined in Meta.indexes.
# Meta.indexes is the modern, recommended way to define custom indexes on model fields.
# It is more flexible and powerful than the legacy index_together option, which is now removed.
# By using Meta.indexes, we ensure compatibility with current and future Django versions.
# The earlier code using index_together is replaced with this.
for index in model_cls._meta.indexes:
if field.name in index.fields:
expected_index_causes.append(f'indexes[{tuple(index.fields)}]')

if field._unique and field.null:
# This is implemented using a (filtered) unique index (not a constraint) to get ANSI NULL behaviour
expected_index_causes.append('unique=True & null=True')
Expand Down
59 changes: 32 additions & 27 deletions testapp/tests/test_multiple_databases.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,9 @@
from django import VERSION
from django.core.exceptions import ValidationError
from django.db import OperationalError
from django.db.backends.sqlite3.operations import DatabaseOperations
from django.db import connections
from django.test import TestCase, skipUnlessDBFeature

from ..models import BinaryData, Pizza, Topping

if VERSION >= (3, 2):
from ..models import TestCheckConstraintWithUnicode

Expand All @@ -21,29 +19,36 @@
)
class TestMultpleDatabases(TestCase):
databases = ['default', 'sqlite']

def test_in_split_parameter_list_as_sql(self):
# Issue: https://github.com/microsoft/mssql-django/issues/92

# Mimic databases that have a limit on parameters (e.g. Oracle DB)
old_max_in_list_size = DatabaseOperations.max_in_list_size
DatabaseOperations.max_in_list_size = lambda self: 100

mssql_iterations = 3000
Pizza.objects.bulk_create([Pizza() for _ in range(mssql_iterations)])
Topping.objects.bulk_create([Topping() for _ in range(mssql_iterations)])
prefetch_result = Pizza.objects.prefetch_related('toppings')
self.assertEqual(len(prefetch_result), mssql_iterations)

# Different iterations since SQLite has max host parameters of 999 for versions prior to 3.32.0
# Info about limit: https://www.sqlite.org/limits.html
sqlite_iterations = 999
Pizza.objects.using('sqlite').bulk_create([Pizza() for _ in range(sqlite_iterations)])
Topping.objects.using('sqlite').bulk_create([Topping() for _ in range(sqlite_iterations)])
prefetch_result_sqlite = Pizza.objects.using('sqlite').prefetch_related('toppings')
self.assertEqual(len(prefetch_result_sqlite), sqlite_iterations)

DatabaseOperations.max_in_list_size = old_max_in_list_size
#for versions >=5.1 it is throwing error due to circular import of DatabaseOperations instead used 'connection.ops'
def test_in_split_parameter_list_as_sql(self):
# Issue: https://github.com/microsoft/mssql-django/issues/92

# Mimic databases that have a limit on parameters (e.g. Oracle DB)
mssql_ops = connections['default'].ops
old_max_in_list_size = mssql_ops.max_in_list_size
mssql_ops.max_in_list_size = lambda: 100

mssql_iterations = 3000
Pizza.objects.bulk_create([Pizza() for _ in range(mssql_iterations)])
Topping.objects.bulk_create([Topping() for _ in range(mssql_iterations)])
prefetch_result = Pizza.objects.prefetch_related('toppings')
self.assertEqual(len(prefetch_result), mssql_iterations)

# Different iterations since SQLite has max host parameters of 999 for versions prior to 3.32.0
# Info about limit: https://www.sqlite.org/limits.html
sqlite_ops = connections['sqlite'].ops
old_sqlite_max_in_list_size = sqlite_ops.max_in_list_size
sqlite_ops.max_in_list_size = lambda: 999

sqlite_iterations = 999
Pizza.objects.using('sqlite').bulk_create([Pizza() for _ in range(sqlite_iterations)])
Topping.objects.using('sqlite').bulk_create([Topping() for _ in range(sqlite_iterations)])
prefetch_result_sqlite = Pizza.objects.using('sqlite').prefetch_related('toppings')
self.assertEqual(len(prefetch_result_sqlite), sqlite_iterations)

# Restore original methods
mssql_ops.max_in_list_size = old_max_in_list_size
sqlite_ops.max_in_list_size = old_sqlite_max_in_list_size

def test_binaryfield_init(self):
binary_data = b'\x00\x46\xFE'
Expand Down Expand Up @@ -94,4 +99,4 @@ def test_queryset_bulk_update(self):
for obj in objs:
obj.binary = None
BinaryData.objects.using('sqlite').bulk_update(objs, ["binary"])
self.assertCountEqual(BinaryData.objects.using('sqlite').filter(binary__isnull=True), objs)
self.assertCountEqual(BinaryData.objects.using('sqlite').filter(binary__isnull=True), objs)
5 changes: 4 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#gave support to django version 5.1
[tox]
envlist =
{py36,py37,py38,py39}-django32,
{py38, py39, py310}-django40,
{py38, py39, py310}-django41,
{py38, py39, py310}-django42,
{py310, py311, py312}-django50
{py310, py311, py312}-django50,
{py310, py311, py312,py313}-django51,

[testenv]
allowlist_externals =
Expand All @@ -23,3 +25,4 @@ deps =
django41: django>=4.1a1,<4.2
django42: django>=4.2,<4.3
django50: django>=5.0,<5.1
django51: django>=5.1,<5.2
Loading