Skip to content

Commit 79a0796

Browse files
committed
Fix error from altering from unique, nullable to non-nullable during migration (microsoft#163)
1 parent ce3f445 commit 79a0796

File tree

2 files changed

+59
-5
lines changed

2 files changed

+59
-5
lines changed

mssql/schema.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import binascii
55
import datetime
66

7+
from collections import defaultdict
8+
79
from django.db.backends.base.schema import (
810
BaseDatabaseSchemaEditor,
911
_is_relevant_relation,
@@ -92,6 +94,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
9294
sql_create_unique_null = "CREATE UNIQUE INDEX %(name)s ON %(table)s(%(columns)s) " \
9395
"WHERE %(columns)s IS NOT NULL"
9496

97+
_deferred_unique_indexes = defaultdict(list)
98+
9599
def _alter_column_default_sql(self, model, old_field, new_field, drop=False):
96100
"""
97101
Hook to specialize column default alteration.
@@ -279,6 +283,15 @@ def _db_table_delete_constraint_sql(self, template, db_table, name):
279283
include=''
280284
)
281285

286+
def _delete_deferred_unique_indexes_for_field(self, field):
287+
deferred_statements = self._deferred_unique_indexes.get(str(field), [])
288+
for stmt in deferred_statements:
289+
if stmt in self.deferred_sql:
290+
self.deferred_sql.remove(stmt)
291+
292+
def _add_deferred_unique_index_for_field(self, field, statement):
293+
self._deferred_unique_indexes[str(field)].append(statement)
294+
282295
def _alter_field(self, model, old_field, new_field, old_type, new_type,
283296
old_db_params, new_db_params, strict=False):
284297
"""Actually perform a "physical" (non-ManyToMany) field update."""
@@ -542,6 +555,7 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
542555
self.execute(self._create_unique_sql(model, [new_field]))
543556
else:
544557
self.execute(self._create_unique_sql(model, [new_field.column]))
558+
self._delete_deferred_unique_indexes_for_field(new_field)
545559
# Added an index?
546560
# constraint will no longer be used in lieu of an index. The following
547561
# lines from the truth table show all True cases; the rest are False:
@@ -574,6 +588,7 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
574588
self.execute(self._create_unique_sql(model, [old_field]))
575589
else:
576590
self.execute(self._create_unique_sql(model, columns=[old_field.column]))
591+
self._delete_deferred_unique_indexes_for_field(old_field)
577592
else:
578593
if django_version >= (4, 0):
579594
for field_names in model._meta.unique_together:
@@ -846,9 +861,11 @@ def add_field(self, model, field):
846861
not field.many_to_many and field.null and field.unique):
847862

848863
definition = definition.replace(' UNIQUE', '')
849-
self.deferred_sql.append(self._create_index_sql(
864+
statement = self._create_index_sql(
850865
model, [field], sql=self.sql_create_unique_null, suffix="_uniq"
851-
))
866+
)
867+
self.deferred_sql.append(statement)
868+
self._add_deferred_unique_index_for_field(field, statement)
852869

853870
# Check constraints can go on the column SQL here
854871
db_params = field.db_parameters(connection=self.connection)
@@ -1012,9 +1029,11 @@ def create_model(self, model):
10121029
not field.many_to_many and field.null and field.unique):
10131030

10141031
definition = definition.replace(' UNIQUE', '')
1015-
self.deferred_sql.append(self._create_index_sql(
1032+
statement = self._create_index_sql(
10161033
model, [field], sql=self.sql_create_unique_null, suffix="_uniq"
1017-
))
1034+
)
1035+
self.deferred_sql.append(statement)
1036+
self._add_deferred_unique_index_for_field(field, statement)
10181037

10191038
# Check constraints can go on the column SQL here
10201039
db_params = field.db_parameters(connection=self.connection)

testapp/tests/test_indexes.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import django.db
44
from django import VERSION
55
from django.apps import apps
6-
from django.db import models
6+
from django.db import models, migrations
7+
from django.db.migrations.migration import Migration
8+
from django.db.migrations.state import ProjectState
79
from django.db.models import UniqueConstraint
810
from django.db.utils import DEFAULT_DB_ALIAS, ConnectionHandler, ProgrammingError
911
from django.test import TestCase
@@ -175,3 +177,36 @@ def test_unique_index_dropped(self):
175177
editor.alter_field(Choice, old_field, new_field, strict=True)
176178
except ProgrammingError:
177179
self.fail("Unique indexes not being dropped")
180+
181+
class TestAddAndAlterUniqueIndex(TestCase):
182+
183+
def test_alter_unique_nullable_to_non_nullable(self):
184+
"""
185+
Test a single migration that creates a field with unique=True and null=True and then alters
186+
the field to set null=False. See https://github.com/microsoft/mssql-django/issues/22
187+
"""
188+
operations = [
189+
migrations.CreateModel(
190+
"TestAlterNullableInUniqueField",
191+
[
192+
("id", models.AutoField(primary_key=True)),
193+
("a", models.CharField(max_length=4, unique=True, null=True)),
194+
]
195+
),
196+
migrations.AlterField(
197+
"testalternullableinuniquefield",
198+
"a",
199+
models.CharField(max_length=4, unique=True)
200+
)
201+
]
202+
203+
project_state = ProjectState()
204+
new_state = project_state.clone()
205+
migration = Migration("name", "testapp")
206+
migration.operations = operations
207+
208+
try:
209+
with connection.schema_editor(atomic=True) as editor:
210+
migration.apply(new_state, editor)
211+
except django.db.utils.ProgrammingError as e:
212+
self.fail('Check if can alter field from unique, nullable to unique non-nullable for issue #23, AlterField failed with exception: %s' % e)

0 commit comments

Comments
 (0)