-
Notifications
You must be signed in to change notification settings - Fork 50
Description
Problem
Unique constraints which are now implemented using a filtered UNIQUE INDEX (to get desired behaviour for multiple NULLs) are not correctly reinstated after an AlterField e.g. if the column type is changed.
This happens in at least these cases:
- after changing the type of a field which has
null=Trueandunique=True - after changing the type of a field involved in a
unique_together
The constraint is reinstated using ALTER TABLE ... ADD CONSTRAINT instead of CREATE UNIQUE INDEX ... WHERE x is NOT NULL
This means the unique constraint is no longer getting the ANSI-compliant NULL behaviour which was recently introduced by PR #1 (for single columns) and PR #24 (for unique_together) to allow for multiple NULL values.
Symptoms
If there are already multiple NULL values in the table, the migration will fail because reinstating the over-strict constraint fails with an error like this:
django.db.utils.IntegrityError: ('23000', "[23000] [Microsoft][ODBC Driver 13 for SQL Server][SQL Server]The CREATE UNIQUE INDEX statement terminated because a duplicate key was found for the object name 'dbo.myapp_foomodel' and the index name 'myapp_foomodel_field_a_d7c02f64_uniq'. The duplicate key value is (<NULL>). (1505) (SQLExecDirectW); [23000] [Microsoft][ODBC Driver 13 for SQL Server][SQL Server]Could not create constraint or index. See previous errors. (1750); [23000] [Microsoft][ODBC Driver 13 for SQL Server][SQL Server]The statement has been terminated. (3621)")
(note: despite the error message saying CREATE UNIQUE INDEX this error is thrown during execution of ALTER TABLE ... ADD CONSTRAINT - presumably because internally in MSSQL this results in running CREATE UNIQUE INDEX without any conditions/filter)
Or if there aren't multiple NULLs then the migration may 'succeed' but:
- have the wrong runtime behaviour with multiple NULLs
- potentially fail on a future migration step - for example in the 2nd case with
unique_together, if in future anAlterUniqueTogethertries to add/remove a field, it will first try to delete the existing constraint as if it were aUNIQUE INDEX(which it should be) but actually it's aCONSTRAINTso the later migration will fail like this:
django.db.utils.ProgrammingError: ('42000', "[42000] [Microsoft][ODBC Driver 13 for SQL Server][SQL Server]An explicit DROP INDEX is not allowed on index 'bar_baz_deadbeef_uniq'. It is being used for UNIQUE KEY constraint enforcement. (3723) (SQLExecDirectW)")
Reproduction steps
I can reproduce this in a minimal Django project with using:
Django==2.2.10
django-mssql-backend==2.8.0
pyodbc==4.0.30
pytz==2019.3
sqlparse==0.3.0
- Add the following model and run
makemigrationsand thenmigrate
class FooModel(models.Model):
field_a = models.CharField(max_length=10, null=True, unique=True)
- Create 2 rows which both have
field_aasNULL - Edit
max_lengthto say15,makemigrationsagain and runmigrate - This explodes with an error as in the first example above
To repro the 2nd example:
- Add
field_x = models.CharField(max_length=10)which is in aunique_togetherwith at least 1 other field e.g.field_y - Then do
AlterFieldonfield_xto change it tomax_length=15(this will succeed if there are no rows in the table) - Then do
AlterUniqueTogetherto add a 3rd fieldfield_zto theunique_together - This explodes with an error like the second example above
Cause
As far as I can tell the issue is that #24 changed the way such unique indexes were implemented, but did not ensure that _alter_field reinstates them in the same way, after they are temporarily dropped in order to make changes to the field.
Proposed solution
In _alter_field under # Restore an index, SQL Server requires explicit restoration, the handling of unique=True/unique_together needs to reinstate filtered UNIQUE INDEX rather than a CONSTRAINT in the relevant cases.
I am working on a PR for this.