diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index af4ddfe07a..2c353fc2f9 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -542,12 +542,7 @@ class BaseDatabaseSchemaEditor(object): self.execute(self._delete_constraint_sql(self.sql_delete_check, model, constraint_name)) # Have they renamed the column? if old_field.column != new_field.column: - self.execute(self.sql_rename_column % { - "table": self.quote_name(model._meta.db_table), - "old_column": self.quote_name(old_field.column), - "new_column": self.quote_name(new_field.column), - "type": new_type, - }) + self.execute(self._rename_field_sql(model._meta.db_table, old_field, new_field, new_type)) # Next, start accumulating actions to do actions = [] null_actions = [] @@ -864,6 +859,14 @@ class BaseDatabaseSchemaEditor(object): output.append(self._create_index_sql(model, fields, suffix="_idx")) return output + def _rename_field_sql(self, table, old_field, new_field, new_type): + return self.sql_rename_column % { + "table": self.quote_name(table), + "old_column": self.quote_name(old_field.column), + "new_column": self.quote_name(new_field.column), + "type": new_type, + } + def _create_fk_sql(self, model, field, suffix): from_table = model._meta.db_table from_column = field.column diff --git a/django/db/backends/mysql/schema.py b/django/db/backends/mysql/schema.py index 7c409598ae..ca650ac1af 100644 --- a/django/db/backends/mysql/schema.py +++ b/django/db/backends/mysql/schema.py @@ -80,10 +80,21 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): ) return super(DatabaseSchemaEditor, self)._delete_composed_index(model, fields, *args) - def _alter_column_type_sql(self, table, old_field, new_field, new_type): - # Keep null property of old field, if it has changed, it will be handled separately - if old_field.null: + def _set_field_new_type_null_status(self, field, new_type): + """ + Keep the null property of the old field. If it has changed, it will be + handled separately. + """ + if field.null: new_type += " NULL" else: new_type += " NOT NULL" + return new_type + + def _alter_column_type_sql(self, table, old_field, new_field, new_type): + new_type = self._set_field_new_type_null_status(old_field, new_type) return super(DatabaseSchemaEditor, self)._alter_column_type_sql(table, old_field, new_field, new_type) + + def _rename_field_sql(self, table, old_field, new_field, new_type): + new_type = self._set_field_new_type_null_status(old_field, new_type) + return super(DatabaseSchemaEditor, self)._rename_field_sql(table, old_field, new_field, new_type) diff --git a/docs/releases/1.7.9.txt b/docs/releases/1.7.9.txt new file mode 100644 index 0000000000..bca875803c --- /dev/null +++ b/docs/releases/1.7.9.txt @@ -0,0 +1,10 @@ +========================== +Django 1.7.9 release notes +========================== + +*Under development* + +Django 1.7.9 fixes several bugs in 1.7.8. + +* Prevented the loss of ``null``/``not null`` column properties during field + renaming of MySQL databases (:ticket:`24817`). diff --git a/docs/releases/1.8.3.txt b/docs/releases/1.8.3.txt index 5adea05dd1..3bc13a2656 100644 --- a/docs/releases/1.8.3.txt +++ b/docs/releases/1.8.3.txt @@ -25,3 +25,6 @@ Bugfixes * Fixed a regression which caused template context processors to overwrite variables set on a ``RequestContext`` after it's created (:ticket:`24847`). + +* Prevented the loss of ``null``/``not null`` column properties during field + renaming of MySQL databases (:ticket:`24817`). diff --git a/docs/releases/index.txt b/docs/releases/index.txt index c9ef3d0c92..8e3e7bbfd4 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -42,6 +42,7 @@ versions of the documentation contain the release notes for any later releases. .. toctree:: :maxdepth: 1 + 1.7.9 1.7.8 1.7.7 1.7.6 diff --git a/tests/schema/models.py b/tests/schema/models.py index 07b9496e14..d50caf9a98 100644 --- a/tests/schema/models.py +++ b/tests/schema/models.py @@ -87,6 +87,14 @@ class Note(models.Model): apps = new_apps +class NoteRename(models.Model): + detail_info = models.TextField() + + class Meta: + apps = new_apps + db_table = "schema_note" + + class Tag(models.Model): title = models.CharField(max_length=255) slug = models.SlugField(unique=True) diff --git a/tests/schema/tests.py b/tests/schema/tests.py index ba20a8a9ac..5128c9ec1e 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -20,8 +20,8 @@ from django.test import TransactionTestCase, skipIfDBFeature from .fields import CustomManyToManyField, InheritedManyToManyField from .models import ( Author, AuthorWithDefaultHeight, AuthorWithEvenLongerName, Book, BookWeak, - BookWithLongName, BookWithO2O, BookWithSlug, Note, Tag, TagIndexed, - TagM2MTest, TagUniqueRename, Thing, UniqueTest, new_apps, + BookWithLongName, BookWithO2O, BookWithSlug, Note, NoteRename, Tag, + TagIndexed, TagM2MTest, TagUniqueRename, Thing, UniqueTest, new_apps, ) @@ -751,6 +751,26 @@ class SchemaTests(TransactionTestCase): self.assertEqual(columns['display_name'][0], "CharField") self.assertNotIn("name", columns) + @skipIfDBFeature('interprets_empty_strings_as_nulls') + def test_rename_keep_null_status(self): + """ + Renaming a field shouldn't affect the not null status. + """ + with connection.schema_editor() as editor: + editor.create_model(Note) + with self.assertRaises(IntegrityError): + Note.objects.create(info=None) + old_field = Note._meta.get_field("info") + new_field = TextField() + new_field.set_attributes_from_name("detail_info") + with connection.schema_editor() as editor: + editor.alter_field(Note, old_field, new_field, strict=True) + columns = self.column_classes(Note) + self.assertEqual(columns['detail_info'][0], "TextField") + self.assertNotIn("info", columns) + with self.assertRaises(IntegrityError): + NoteRename.objects.create(detail_info=None) + def _test_m2m_create(self, M2MFieldClass): """ Tests M2M fields on models during creation