From 349e58f8ccf43953f8b5cd7090a63d4f5a5bfbb0 Mon Sep 17 00:00:00 2001 From: Ignacio Vissani Date: Fri, 25 Sep 2020 17:54:52 -0300 Subject: [PATCH 1/6] Migration that alters unique nullable field to non-nullable This migration fails --- ...008_test_alter_nullable_in_unique_field.py | 24 +++++++++++++++++++ testapp/models.py | 8 +++++++ 2 files changed, 32 insertions(+) create mode 100644 testapp/migrations/0008_test_alter_nullable_in_unique_field.py diff --git a/testapp/migrations/0008_test_alter_nullable_in_unique_field.py b/testapp/migrations/0008_test_alter_nullable_in_unique_field.py new file mode 100644 index 00000000..921a426d --- /dev/null +++ b/testapp/migrations/0008_test_alter_nullable_in_unique_field.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.4 on 2020-04-20 14:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('testapp', '0007_test_remove_onetoone_field_part2'), + ] + + operations = [ + migrations.CreateModel( + name='TestAlterNullableInUniqueField', + fields=[ + ('a', models.CharField(max_length=50, null=True, unique=True)), + ], + ), + migrations.AlterField( + model_name='testalternullableinuniquefield', + name='a', + field=models.CharField(max_length=50, unique=True), + ) + ] diff --git a/testapp/models.py b/testapp/models.py index c87f797b..4357d91f 100644 --- a/testapp/models.py +++ b/testapp/models.py @@ -71,3 +71,11 @@ class TestRemoveOneToOneFieldModel(models.Model): # thats already is removed. # b = models.OneToOneField('self', on_delete=models.SET_NULL, null=True) a = models.CharField(max_length=50) + + +class TestAlterNullableInUniqueField(models.Model): + """ Model used to test a single migration that creates a field with unique=True and null=True and then alters + the field to set null=False. This is a common use case when you want to add a non-nullable unique field to a + pre-existing model. In order to make that work you need to first create the unique field as nullable, then + populate the field for every pre-existing instance, and then alter the field to set it to non-nullaable. """ + a = models.CharField(max_length=50, unique=True, null=True) From a25593b1c0791e85bbf877a6b1fa2f2088aaa8be Mon Sep 17 00:00:00 2001 From: Ignacio Vissani Date: Sat, 10 Oct 2020 23:23:07 -0300 Subject: [PATCH 2/6] Delete deferred unique index creation when already created in the same migration --- sql_server/pyodbc/schema.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/sql_server/pyodbc/schema.py b/sql_server/pyodbc/schema.py index 9abcbd04..a4968718 100644 --- a/sql_server/pyodbc/schema.py +++ b/sql_server/pyodbc/schema.py @@ -18,6 +18,8 @@ from django.db.transaction import TransactionManagementError from django.utils.encoding import force_str +from collections import defaultdict + class Statement(DjStatement): def __hash__(self): @@ -68,6 +70,8 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_create_unique_null = "CREATE UNIQUE INDEX %(name)s ON %(table)s(%(columns)s) " \ "WHERE %(columns)s IS NOT NULL" + _deferred_unique_indexes = defaultdict(list) + def _alter_column_default_sql(self, model, old_field, new_field, drop=False): """ Hook to specialize column default alteration. @@ -236,6 +240,12 @@ def alter_db_table(self, model, old_db_table, new_db_table): return super().alter_db_table(model, old_db_table, new_db_table) + def _delete_deferred_unique_indexes_for_field(self, field_name): + deferred_statements = self._deferred_unique_indexes.get(field_name, []) + for stmt in deferred_statements: + if stmt in self.deferred_sql: + self.deferred_sql.remove(stmt) + def _alter_field(self, model, old_field, new_field, old_type, new_type, old_db_params, new_db_params, strict=False): """Actually perform a "physical" (non-ManyToMany) field update.""" @@ -449,6 +459,7 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, ) else: self.execute(self._create_unique_sql(model, [new_field.column])) + self._delete_deferred_unique_indexes_for_field(old_field) # Added an index? # constraint will no longer be used in lieu of an index. The following # lines from the truth table show all True cases; the rest are False: @@ -477,6 +488,7 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, ) else: self.execute(self._create_unique_sql(model, columns=[old_field.column])) + self._delete_deferred_unique_indexes_for_field(old_field) else: for fields in model._meta.unique_together: columns = [model._meta.get_field(field).column for field in fields] @@ -644,9 +656,11 @@ def add_field(self, model, field): not field.many_to_many and field.null and field.unique): definition = definition.replace(' UNIQUE', '') - self.deferred_sql.append(self._create_index_sql( + statement = self._create_index_sql( model, [field], sql=self.sql_create_unique_null, suffix="_uniq" - )) + ) + self.deferred_sql.append(statement) + self._deferred_unique_indexes[field].append(statement) # Check constraints can go on the column SQL here db_params = field.db_parameters(connection=self.connection) @@ -750,9 +764,11 @@ def create_model(self, model): not field.many_to_many and field.null and field.unique): definition = definition.replace(' UNIQUE', '') - self.deferred_sql.append(self._create_index_sql( + statement = self._create_index_sql( model, [field], sql=self.sql_create_unique_null, suffix="_uniq" - )) + ) + self.deferred_sql.append(statement) + self._deferred_unique_indexes[field].append(statement) # Check constraints can go on the column SQL here db_params = field.db_parameters(connection=self.connection) From e1c19540f1a284b40b2500d3f473379055b2c92d Mon Sep 17 00:00:00 2001 From: Ignacio Vissani Date: Sun, 11 Oct 2020 00:09:54 -0300 Subject: [PATCH 3/6] Fix old_field -> new_field --- sql_server/pyodbc/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sql_server/pyodbc/schema.py b/sql_server/pyodbc/schema.py index a4968718..bbdc56bb 100644 --- a/sql_server/pyodbc/schema.py +++ b/sql_server/pyodbc/schema.py @@ -459,7 +459,7 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type, ) else: self.execute(self._create_unique_sql(model, [new_field.column])) - self._delete_deferred_unique_indexes_for_field(old_field) + self._delete_deferred_unique_indexes_for_field(new_field) # Added an index? # constraint will no longer be used in lieu of an index. The following # lines from the truth table show all True cases; the rest are False: From 3c95a5e67c7003d40a3669df37cb6d3afacda5dd Mon Sep 17 00:00:00 2001 From: Ignacio Vissani Date: Tue, 13 Oct 2020 09:47:24 -0300 Subject: [PATCH 4/6] use str of field as index --- sql_server/pyodbc/schema.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/sql_server/pyodbc/schema.py b/sql_server/pyodbc/schema.py index bbdc56bb..262d4c81 100644 --- a/sql_server/pyodbc/schema.py +++ b/sql_server/pyodbc/schema.py @@ -240,12 +240,15 @@ def alter_db_table(self, model, old_db_table, new_db_table): return super().alter_db_table(model, old_db_table, new_db_table) - def _delete_deferred_unique_indexes_for_field(self, field_name): - deferred_statements = self._deferred_unique_indexes.get(field_name, []) + def _delete_deferred_unique_indexes_for_field(self, field): + deferred_statements = self._deferred_unique_indexes.get(str(field), []) for stmt in deferred_statements: if stmt in self.deferred_sql: self.deferred_sql.remove(stmt) + def _add_deferred_unique_index_for_field(self, field, statement): + self._deferred_unique_indexes[str(field)].append(statement) + def _alter_field(self, model, old_field, new_field, old_type, new_type, old_db_params, new_db_params, strict=False): """Actually perform a "physical" (non-ManyToMany) field update.""" @@ -660,7 +663,7 @@ def add_field(self, model, field): model, [field], sql=self.sql_create_unique_null, suffix="_uniq" ) self.deferred_sql.append(statement) - self._deferred_unique_indexes[field].append(statement) + self._add_deferred_unique_index_for_field(field, statement) # Check constraints can go on the column SQL here db_params = field.db_parameters(connection=self.connection) @@ -768,7 +771,7 @@ def create_model(self, model): model, [field], sql=self.sql_create_unique_null, suffix="_uniq" ) self.deferred_sql.append(statement) - self._deferred_unique_indexes[field].append(statement) + self._add_deferred_unique_index_for_field(field, statement) # Check constraints can go on the column SQL here db_params = field.db_parameters(connection=self.connection) From de0584a3ec54882c5c446f59d1e913c680abb243 Mon Sep 17 00:00:00 2001 From: Ignacio Vissani Date: Wed, 17 Feb 2021 16:04:41 -0300 Subject: [PATCH 5/6] Fix delete unique --- sql_server/pyodbc/schema.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sql_server/pyodbc/schema.py b/sql_server/pyodbc/schema.py index 262d4c81..3ff27cfd 100644 --- a/sql_server/pyodbc/schema.py +++ b/sql_server/pyodbc/schema.py @@ -69,6 +69,7 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_rename_table = "EXEC sp_rename %(old_table)s, %(new_table)s" sql_create_unique_null = "CREATE UNIQUE INDEX %(name)s ON %(table)s(%(columns)s) " \ "WHERE %(columns)s IS NOT NULL" + sql_delete_unique = sql_delete_index _deferred_unique_indexes = defaultdict(list) From 964df2cdf8e6a719e1d21b1c46e7751cae08dc29 Mon Sep 17 00:00:00 2001 From: Ignacio Vissani Date: Wed, 17 Feb 2021 17:00:12 -0300 Subject: [PATCH 6/6] Revert "Fix delete unique" This reverts commit de0584a3ec54882c5c446f59d1e913c680abb243. --- sql_server/pyodbc/schema.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sql_server/pyodbc/schema.py b/sql_server/pyodbc/schema.py index 3ff27cfd..262d4c81 100644 --- a/sql_server/pyodbc/schema.py +++ b/sql_server/pyodbc/schema.py @@ -69,7 +69,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_rename_table = "EXEC sp_rename %(old_table)s, %(new_table)s" sql_create_unique_null = "CREATE UNIQUE INDEX %(name)s ON %(table)s(%(columns)s) " \ "WHERE %(columns)s IS NOT NULL" - sql_delete_unique = sql_delete_index _deferred_unique_indexes = defaultdict(list)