Skip to content

Commit cdad07b

Browse files
authored
Merge pull request #400 from microsoft/164
Add function to keep index when alter FK
2 parents b2eec58 + 6fd9a0b commit cdad07b

File tree

2 files changed

+177
-5
lines changed

2 files changed

+177
-5
lines changed

mssql/schema.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from django import VERSION as django_version
2222
from django.db.models import NOT_PROVIDED, Index, UniqueConstraint
2323
from django.db.models.fields import AutoField, BigAutoField
24+
from django.db.models.fields.related import ForeignKey
2425
from django.db.models.sql.where import AND
2526
from django.db.transaction import TransactionManagementError
2627
from django.utils.encoding import force_str
@@ -183,7 +184,7 @@ def _alter_column_database_default_sql(
183184
argument) a default to new_field's column.
184185
"""
185186
column = self.quote_name(new_field.column)
186-
187+
187188
if drop:
188189
# SQL Server requires the name of the default constraint
189190
result = self.execute(
@@ -426,10 +427,20 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
426427
)
427428
):
428429
# Drop index, SQL Server requires explicit deletion
429-
if not hasattr(new_field, 'db_constraint') or not new_field.db_constraint:
430-
index_names = self._constraint_names(model, [old_field.column], index=True)
431-
for index_name in index_names:
432-
self.execute(self._delete_constraint_sql(self.sql_delete_index, model, index_name))
430+
if (
431+
not hasattr(new_field, "db_constraint")
432+
or not new_field.db_constraint
433+
):
434+
if(django_version < (4, 2)
435+
or (
436+
not isinstance(new_field, ForeignKey)
437+
or type(new_field.db_comment) == type(None)
438+
or "fk_on_delete_keep_index" not in new_field.db_comment
439+
)
440+
):
441+
index_names = self._constraint_names(model, [old_field.column], index=True)
442+
for index_name in index_names:
443+
self.execute(self._delete_constraint_sql(self.sql_delete_index, model, index_name))
433444

434445
fk_names = self._constraint_names(model, [old_field.column], foreign_key=True)
435446
if strict and len(fk_names) != 1:
@@ -911,6 +922,13 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
911922
self.connection.close()
912923

913924
def _delete_indexes(self, model, old_field, new_field):
925+
if (
926+
django_version >= (4, 2)
927+
and isinstance(new_field, ForeignKey)
928+
and type(new_field.db_comment) != type(None)
929+
and "fk_on_delete_keep_index" in new_field.db_comment
930+
):
931+
return
914932
index_columns = []
915933
index_names = []
916934
if old_field.db_index and new_field.db_index:

testapp/tests/test_indexes.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from django.db.models import UniqueConstraint
1010
from django.db.utils import DEFAULT_DB_ALIAS, ConnectionHandler, ProgrammingError
1111
from django.test import TestCase
12+
from unittest import skipIf
1213

1314
from . import get_constraints
1415
from ..models import (
@@ -210,3 +211,156 @@ def test_alter_unique_nullable_to_non_nullable(self):
210211
migration.apply(new_state, editor)
211212
except django.db.utils.ProgrammingError as e:
212213
self.fail('Check if can alter field from unique, nullable to unique non-nullable for issue #23, AlterField failed with exception: %s' % e)
214+
215+
class TestKeepIndexWithDbcomment(TestCase):
216+
def _find_key_with_type_idx(self, input_dict):
217+
for key, value in input_dict.items():
218+
if value.get("type") == "idx":
219+
return key
220+
return None
221+
222+
@skipIf(VERSION < (4, 2), "db_comment not available before 4.2")
223+
def test_drop_foreignkey(self):
224+
app_label = "test_drop_foreignkey"
225+
operations = [
226+
migrations.CreateModel(
227+
name="brand",
228+
fields=[
229+
("id", models.AutoField(primary_key=True)),
230+
("name", models.CharField(max_length=100)),
231+
],
232+
),
233+
migrations.CreateModel(
234+
name="car1",
235+
fields=[
236+
("id", models.AutoField(primary_key=True)),
237+
(
238+
"brand",
239+
models.ForeignKey(
240+
on_delete=django.db.models.deletion.CASCADE,
241+
to="test_drop_foreignkey.brand",
242+
related_name="car1",
243+
db_constraint=True,
244+
),
245+
),
246+
],
247+
),
248+
migrations.CreateModel(
249+
name="car2",
250+
fields=[
251+
("id", models.AutoField(primary_key=True)),
252+
(
253+
"brand",
254+
models.ForeignKey(
255+
on_delete=django.db.models.deletion.CASCADE,
256+
to="test_drop_foreignkey.brand",
257+
related_name="car2",
258+
db_constraint=True,
259+
),
260+
),
261+
],
262+
),
263+
migrations.CreateModel(
264+
name="car3",
265+
fields=[
266+
("id", models.AutoField(primary_key=True)),
267+
(
268+
"brand",
269+
models.ForeignKey(
270+
on_delete=django.db.models.deletion.CASCADE,
271+
to="test_drop_foreignkey.brand",
272+
related_name="car3",
273+
db_constraint=True,
274+
),
275+
),
276+
],
277+
),
278+
]
279+
migration = Migration("name", app_label)
280+
migration.operations = operations
281+
with connection.schema_editor(atomic=True) as editor:
282+
project_state = migration.apply(ProjectState(), editor)
283+
284+
alter_fk_car1 = migrations.AlterField(
285+
model_name="car1",
286+
name="brand",
287+
field=models.ForeignKey(
288+
to="test_drop_foreignkey.brand",
289+
on_delete=django.db.models.deletion.CASCADE,
290+
db_constraint=False,
291+
related_name="car1",
292+
),
293+
)
294+
alter_fk_car2 = migrations.AlterField(
295+
model_name="car2",
296+
name="brand",
297+
field=models.ForeignKey(
298+
to="test_drop_foreignkey.brand",
299+
on_delete=django.db.models.deletion.CASCADE,
300+
db_constraint=False,
301+
related_name="car2",
302+
db_comment=""
303+
),
304+
)
305+
alter_fk_car3 = migrations.AlterField(
306+
model_name="car3",
307+
name="brand",
308+
field=models.ForeignKey(
309+
to="test_drop_foreignkey.brand",
310+
on_delete=django.db.models.deletion.CASCADE,
311+
db_constraint=False,
312+
related_name="car3",
313+
db_comment="fk_on_delete_keep_index"
314+
),
315+
)
316+
new_state = project_state.clone()
317+
with connection.schema_editor(atomic=True) as editor:
318+
alter_fk_car1.state_forwards("test_drop_foreignkey", new_state)
319+
alter_fk_car1.database_forwards(
320+
"test_drop_foreignkey", editor, project_state, new_state
321+
)
322+
car_index = self._find_key_with_type_idx(
323+
get_constraints(
324+
table_name=new_state.apps.get_model(
325+
"test_drop_foreignkey", "car1"
326+
)._meta.db_table
327+
)
328+
)
329+
# Test alter foreignkey without db_comment field
330+
# The index should be dropped (keep the old behavior)
331+
self.assertIsNone(car_index)
332+
333+
project_state = new_state
334+
new_state = new_state.clone()
335+
with connection.schema_editor(atomic=True) as editor:
336+
alter_fk_car2.state_forwards("test_drop_foreignkey", new_state)
337+
alter_fk_car2.database_forwards(
338+
"test_drop_foreignkey", editor, project_state, new_state
339+
)
340+
car_index = self._find_key_with_type_idx(
341+
get_constraints(
342+
table_name=new_state.apps.get_model(
343+
"test_drop_foreignkey", "car2"
344+
)._meta.db_table
345+
)
346+
)
347+
# Test alter fk with empty db_comment
348+
self.assertIsNone(car_index)
349+
350+
project_state = new_state
351+
new_state = new_state.clone()
352+
with connection.schema_editor(atomic=True) as editor:
353+
alter_fk_car3.state_forwards("test_drop_foreignkey", new_state)
354+
alter_fk_car3.database_forwards(
355+
"test_drop_foreignkey", editor, project_state, new_state
356+
)
357+
car_index = self._find_key_with_type_idx(
358+
get_constraints(
359+
table_name=new_state.apps.get_model(
360+
"test_drop_foreignkey", "car3"
361+
)._meta.db_table
362+
)
363+
)
364+
# Test alter fk with fk_on_delete_keep_index in db_comment
365+
# Index should be preserved in this case
366+
self.assertIsNotNone(car_index)

0 commit comments

Comments
 (0)