From 927d90ee1ee4aa630a5a879b5fd75aa03a3341f7 Mon Sep 17 00:00:00 2001 From: Andriy Sokolovskiy Date: Wed, 27 May 2015 01:18:21 +0300 Subject: [1.7.x] Fixed #24817 -- Prevented loss of null info in MySQL field renaming. Backport of 80ad5472ce4b6ba6e94227422d0727371e97cdf0 from master --- django/db/backends/mysql/schema.py | 17 +++++++++++--- django/db/backends/schema.py | 15 +++++++----- docs/releases/1.7.9.txt | 10 ++++++++ docs/releases/index.txt | 1 + tests/schema/models.py | 8 +++++++ tests/schema/tests.py | 48 +++++++++++++++++++++++++++++++------- 6 files changed, 81 insertions(+), 18 deletions(-) create mode 100644 docs/releases/1.7.9.txt diff --git a/django/db/backends/mysql/schema.py b/django/db/backends/mysql/schema.py index 37c130e908..15286a67a1 100644 --- a/django/db/backends/mysql/schema.py +++ b/django/db/backends/mysql/schema.py @@ -49,10 +49,21 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): 'column': self.quote_name(field.column), }, [effective_default]) - 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/django/db/backends/schema.py b/django/db/backends/schema.py index 6febdb134d..0d9ebf7d89 100644 --- a/django/db/backends/schema.py +++ b/django/db/backends/schema.py @@ -527,12 +527,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 = [] @@ -841,6 +836,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/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/index.txt b/docs/releases/index.txt index 15473f56c6..a574ef67a2 100644 --- a/docs/releases/index.txt +++ b/docs/releases/index.txt @@ -25,6 +25,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 b9331750c4..507acb1a23 100644 --- a/tests/schema/models.py +++ b/tests/schema/models.py @@ -122,6 +122,14 @@ class BookWithSlug(models.Model): db_table = "schema_book" +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 7b4f751852..09a0b7646f 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -1,17 +1,27 @@ import datetime import unittest -from django.test import TransactionTestCase, skipIfDBFeature -from django.db import connection, DatabaseError, IntegrityError, OperationalError -from django.db.models.fields import (BigIntegerField, BinaryField, BooleanField, CharField, - IntegerField, PositiveIntegerField, SlugField, TextField) -from django.db.models.fields.related import ForeignKey, ManyToManyField, OneToOneField +from django.db import ( + DatabaseError, IntegrityError, OperationalError, connection, +) +from django.db.models.fields import ( + BigIntegerField, BinaryField, BooleanField, CharField, IntegerField, + PositiveIntegerField, SlugField, TextField, +) +from django.db.models.fields.related import ( + ForeignKey, ManyToManyField, OneToOneField, +) from django.db.transaction import atomic +from django.test import TransactionTestCase, skipIfDBFeature + from .fields import CustomManyToManyField, InheritedManyToManyField -from .models import (Author, AuthorWithDefaultHeight, AuthorWithM2M, Book, BookWithLongName, - BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename, - UniqueTest, Thing, TagThrough, BookWithM2MThrough, AuthorTag, AuthorWithM2MThrough, - AuthorWithEvenLongerName, BookWeak, Note, BookWithO2O, BookWithoutFK) +from .models import ( + Author, AuthorTag, AuthorWithDefaultHeight, AuthorWithEvenLongerName, + AuthorWithM2M, AuthorWithM2MThrough, Book, BookWeak, BookWithLongName, + BookWithM2M, BookWithM2MThrough, BookWithO2O, BookWithoutFK, BookWithSlug, + Note, NoteRename, Tag, TagIndexed, TagM2MTest, TagThrough, TagUniqueRename, + Thing, UniqueTest, +) class SchemaTests(TransactionTestCase): @@ -754,6 +764,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): """ Tests M2M fields on models during creation -- cgit v1.2.1