Skip to content

Commit b02c821

Browse files
authored
Add capability to convert to and from AutoField and BigAutoField (#128)
* Add support for AutoField BigAutoField migration * Re-adding unit tests for autofield conversions * Check added for identity field conversions. Typo fix delete query.
1 parent 1b30d21 commit b02c821

File tree

5 files changed

+144
-20
lines changed

5 files changed

+144
-20
lines changed

mssql/schema.py

Lines changed: 89 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,20 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor):
6969
sql_delete_column = "ALTER TABLE %(table)s DROP COLUMN %(column)s"
7070
sql_delete_index = "DROP INDEX %(name)s ON %(table)s"
7171
sql_delete_table = """
72-
DECLARE @sql_froeign_constraint_name nvarchar(128)
72+
DECLARE @sql_foreign_constraint_name nvarchar(128)
7373
DECLARE @sql_drop_constraint nvarchar(300)
7474
WHILE EXISTS(SELECT 1
7575
FROM sys.foreign_keys
7676
WHERE referenced_object_id = object_id('%(table)s'))
7777
BEGIN
78-
SELECT TOP 1 @sql_froeign_constraint_name = name
78+
SELECT TOP 1 @sql_foreign_constraint_name = name
7979
FROM sys.foreign_keys
8080
WHERE referenced_object_id = object_id('%(table)s')
8181
SELECT
8282
@sql_drop_constraint = 'ALTER TABLE [' + OBJECT_NAME(parent_object_id) + '] ' +
83-
'DROP CONSTRAINT [' + @sql_froeign_constraint_name + '] '
83+
'DROP CONSTRAINT [' + @sql_foreign_constraint_name + '] '
8484
FROM sys.foreign_keys
85-
WHERE referenced_object_id = object_id('%(table)s') and name = @sql_froeign_constraint_name
85+
WHERE referenced_object_id = object_id('%(table)s') and name = @sql_foreign_constraint_name
8686
exec sp_executesql @sql_drop_constraint
8787
END
8888
DROP TABLE %(table)s
@@ -283,11 +283,19 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
283283
old_db_params, new_db_params, strict=False):
284284
"""Actually perform a "physical" (non-ManyToMany) field update."""
285285

286-
# the backend doesn't support altering from/to (Big)AutoField
287-
# because of the limited capability of SQL Server to edit IDENTITY property
286+
# the backend doesn't support altering a column to/from AutoField as
287+
# SQL Server cannot alter columns to add and remove IDENTITY properties
288+
old_is_auto = False
289+
new_is_auto = False
288290
for t in (AutoField, BigAutoField):
289-
if isinstance(old_field, t) or isinstance(new_field, t):
290-
raise NotImplementedError("the backend doesn't support altering from/to %s." % t.__name__)
291+
if isinstance(old_field, t):
292+
old_is_auto = True
293+
if isinstance(new_field, t):
294+
new_is_auto = True
295+
if (old_is_auto and not new_is_auto) or (not old_is_auto and new_is_auto):
296+
raise NotImplementedError("the backend doesn't support altering from %s to %s." %
297+
(old_field.get_internal_type(), new_field.get_internal_type()))
298+
291299
# Drop any FK constraints, we'll remake them later
292300
fks_dropped = set()
293301
if old_field.remote_field and old_field.db_constraint:
@@ -327,6 +335,17 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
327335
)
328336
for fk_name in rel_fk_names:
329337
self.execute(self._delete_constraint_sql(self.sql_delete_fk, new_rel.related_model, fk_name))
338+
# If working with an AutoField or BigAutoField drop all indexes on the related table
339+
# This is needed when doing ALTER column statements on IDENTITY fields
340+
# https://stackoverflow.com/questions/33429775/sql-server-alter-table-alter-column-giving-set-option-error
341+
for t in (AutoField, BigAutoField):
342+
if isinstance(old_field, t) or isinstance(new_field, t):
343+
index_names = self._constraint_names(model, index=True)
344+
for index_name in index_names:
345+
self.execute(
346+
self._delete_constraint_sql(self.sql_delete_index, model, index_name)
347+
)
348+
break
330349
# Removed an index? (no strict check, as multiple indexes are possible)
331350
# Remove indexes if db_index switched to False or a unique constraint
332351
# will now be used in lieu of an index. The following lines from the
@@ -542,7 +561,8 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
542561
# Restore unique constraints
543562
# Note: if nullable they are implemented via an explicit filtered UNIQUE INDEX (not CONSTRAINT)
544563
# in order to get ANSI-compliant NULL behaviour (i.e. NULL != NULL, multiple are allowed)
545-
if old_field.unique and new_field.unique:
564+
# Note: Don't restore primary keys, we need to re-create those seperately
565+
if old_field.unique and new_field.unique and not new_field.primary_key:
546566
if new_field.null:
547567
self.execute(
548568
self._create_index_sql(
@@ -568,7 +588,47 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
568588
if old_field.column in columns:
569589
condition = ' AND '.join(["[%s] IS NOT NULL" % col for col in columns])
570590
self.execute(self._create_unique_sql(model, columns, condition=condition))
591+
# Restore primary keys
592+
if old_field.primary_key and new_field.primary_key:
593+
self.execute(
594+
self.sql_create_pk % {
595+
"table": self.quote_name(model._meta.db_table),
596+
"name": self.quote_name(
597+
self._create_index_name(model._meta.db_table, [new_field.column], suffix="_pk")
598+
),
599+
"columns": self.quote_name(new_field.column),
600+
}
601+
)
602+
# Restore unqiue_together
603+
# If we have ALTERed an AutoField or BigAutoField we need to recreate all unique_together clauses
604+
for t in (AutoField, BigAutoField):
605+
if isinstance(old_field, t) or isinstance(new_field, t):
606+
for field_names in model._meta.unique_together:
607+
columns = [model._meta.get_field(field).column for field in field_names]
608+
fields = [model._meta.get_field(field) for field in field_names]
609+
condition = ' AND '.join(["[%s] IS NOT NULL" % col for col in columns])
610+
# We need to pass fields instead of columns when using >= Django 4.0 because
611+
# of a backwards incompatible change to _create_unique_sql
612+
if django_version >= (4, 0):
613+
self.execute(
614+
self._create_unique_sql(model, fields, condition=condition)
615+
)
616+
else:
617+
self.execute(
618+
self._create_unique_sql(model, columns, condition=condition)
619+
)
620+
break
621+
571622
# Restore indexes
623+
# If we have ALTERed an AutoField or BigAutoField we need to recreate all indexes
624+
for t in (AutoField, BigAutoField):
625+
if isinstance(old_field, t) or isinstance(new_field, t):
626+
for field in model._meta.fields:
627+
if field.db_index:
628+
self.execute(
629+
self._create_index_sql(model, [field])
630+
)
631+
break
572632
index_columns = []
573633
if old_field.db_index and new_field.db_index:
574634
index_columns.append([old_field])
@@ -626,7 +686,26 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
626686
for sql, params in other_actions:
627687
self.execute(sql, params)
628688
# Restore related_model indexes
629-
self.execute(self._create_index_sql(new_rel.related_model, [new_rel.field]))
689+
for field in new_rel.related_model._meta.fields:
690+
if field.db_index:
691+
self.execute(
692+
self._create_index_sql(new_rel.related_model, [field])
693+
)
694+
# Restore unique_together clauses
695+
for field_names in new_rel.related_model._meta.unique_together:
696+
columns = [new_rel.related_model._meta.get_field(field).column for field in field_names]
697+
fields = [new_rel.related_model._meta.get_field(field) for field in field_names]
698+
condition = ' AND '.join(["[%s] IS NOT NULL" % col for col in columns])
699+
# We need to pass fields instead of columns when using >= Django 4.0 because
700+
# of a backwards incompatible change to _create_unique_sql
701+
if django_version >= (4, 0):
702+
self.execute(
703+
self._create_unique_sql(new_rel.related_model, fields, condition=condition)
704+
)
705+
else:
706+
self.execute(
707+
self._create_unique_sql(new_rel.related_model, columns, condition=condition)
708+
)
630709
# Does it have a foreign key?
631710
if (new_field.remote_field and
632711
(fks_dropped or not old_field.remote_field or not old_field.db_constraint) and
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 3.2.13 on 2022-05-04 01:36
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('testapp', '0019_customer_name_customer_address'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='author',
15+
name='id',
16+
field=models.BigAutoField(primary_key=True, serialize=False),
17+
),
18+
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Generated by Django 3.2.13 on 2022-05-04 01:37
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('testapp', '0020_autofield_to_bigautofield'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='author',
15+
name='id',
16+
field=models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
17+
),
18+
migrations.AlterField(
19+
model_name='editor',
20+
name='id',
21+
field=models.BigAutoField(primary_key=True, serialize=False),
22+
),
23+
migrations.AlterField(
24+
model_name='post',
25+
name='id',
26+
field=models.BigAutoField(primary_key=True, serialize=False),
27+
),
28+
]

testapp/models.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,22 @@
99
from django.db.models import Q
1010
from django.utils import timezone
1111

12+
# We are using this Mixin to test casting of BigAuto and Auto fields
13+
class BigAutoFieldMixin(models.Model):
14+
id = models.BigAutoField(primary_key=True)
15+
16+
class Meta:
17+
abstract = True
1218

1319
class Author(models.Model):
1420
name = models.CharField(max_length=100)
1521

1622

17-
class Editor(models.Model):
23+
class Editor(BigAutoFieldMixin, models.Model):
1824
name = models.CharField(max_length=100)
1925

2026

21-
class Post(models.Model):
27+
class Post(BigAutoFieldMixin, models.Model):
2228
title = models.CharField('title', max_length=255)
2329
author = models.ForeignKey(Author, models.CASCADE)
2430
# Optional secondary author

testapp/settings.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@
124124
'migrations.test_operations.OperationTests.test_add_constraint_percent_escaping',
125125
'migrations.test_operations.OperationTests.test_alter_field_pk',
126126
'migrations.test_operations.OperationTests.test_alter_field_reloads_state_on_fk_with_to_field_target_changes',
127-
'migrations.test_operations.OperationTests.test_autofield_foreignfield_growth',
128127
'schema.tests.SchemaTests.test_alter_auto_field_to_char_field',
129128
'schema.tests.SchemaTests.test_alter_auto_field_to_integer_field',
130129
'schema.tests.SchemaTests.test_alter_implicit_id_to_explicit',
@@ -198,15 +197,9 @@
198197
'expressions.tests.FTimeDeltaTests.test_time_subquery_subtraction',
199198
'expressions.tests.BasicExpressionsTests.test_filtering_on_q_that_is_boolean',
200199
'migrations.test_operations.OperationTests.test_alter_field_reloads_state_on_fk_with_to_field_target_type_change',
201-
'migrations.test_operations.OperationTests.test_autofield__bigautofield_foreignfield_growth',
202-
'migrations.test_operations.OperationTests.test_smallfield_autofield_foreignfield_growth',
203-
'migrations.test_operations.OperationTests.test_smallfield_bigautofield_foreignfield_growth',
204-
'schema.tests.SchemaTests.test_alter_auto_field_quoted_db_column',
205-
'schema.tests.SchemaTests.test_alter_autofield_pk_to_bigautofield_pk_sequence_owner',
206-
'schema.tests.SchemaTests.test_alter_autofield_pk_to_smallautofield_pk_sequence_owner',
207200
'schema.tests.SchemaTests.test_alter_primary_key_quoted_db_table',
208201
'schema.tests.SchemaTests.test_alter_smallint_pk_to_smallautofield_pk',
209-
202+
210203
'annotations.tests.NonAggregateAnnotationTestCase.test_combined_expression_annotation_with_aggregation',
211204
'db_functions.comparison.test_cast.CastTests.test_cast_to_integer',
212205
'db_functions.datetime.test_extract_trunc.DateFunctionTests.test_extract_func',

0 commit comments

Comments
 (0)