From 5792e6a88c1444d4ec84abe62077338ad3765b80 Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Mon, 19 Jan 2015 15:31:23 +0100 Subject: [PATCH] Fixed #24163 -- Removed unique constraint after index on MySQL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks Ɓukasz Harasimowicz for the report. --- django/db/backends/base/schema.py | 24 +++---- docs/releases/1.7.4.txt | 3 + tests/schema/models.py | 10 +++ tests/schema/tests.py | 106 +++++++++++++++++++++++++++++- 4 files changed, 128 insertions(+), 15 deletions(-) diff --git a/django/db/backends/base/schema.py b/django/db/backends/base/schema.py index 17a7a0d458c..09db3f10966 100644 --- a/django/db/backends/base/schema.py +++ b/django/db/backends/base/schema.py @@ -488,18 +488,6 @@ class BaseDatabaseSchemaEditor(object): old_db_params, new_db_params, strict=False): """Actually perform a "physical" (non-ManyToMany) field update.""" - # Has unique been removed? - if old_field.unique and (not new_field.unique or (not old_field.primary_key and new_field.primary_key)): - # Find the unique constraint for this field - constraint_names = self._constraint_names(model, [old_field.column], unique=True) - if strict and len(constraint_names) != 1: - raise ValueError("Found wrong number (%s) of unique constraints for %s.%s" % ( - len(constraint_names), - model._meta.db_table, - old_field.column, - )) - for constraint_name in constraint_names: - self.execute(self._delete_constraint_sql(self.sql_delete_unique, model, constraint_name)) # Drop any FK constraints, we'll remake them later fks_dropped = set() if old_field.rel and old_field.db_constraint: @@ -513,6 +501,18 @@ class BaseDatabaseSchemaEditor(object): for fk_name in fk_names: fks_dropped.add((old_field.column,)) self.execute(self._delete_constraint_sql(self.sql_delete_fk, model, fk_name)) + # Has unique been removed? + if old_field.unique and (not new_field.unique or (not old_field.primary_key and new_field.primary_key)): + # Find the unique constraint for this field + constraint_names = self._constraint_names(model, [old_field.column], unique=True) + if strict and len(constraint_names) != 1: + raise ValueError("Found wrong number (%s) of unique constraints for %s.%s" % ( + len(constraint_names), + model._meta.db_table, + old_field.column, + )) + for constraint_name in constraint_names: + self.execute(self._delete_constraint_sql(self.sql_delete_unique, model, constraint_name)) # Drop incoming FK constraints if we're a primary key and things are going # to change. if old_field.primary_key and new_field.primary_key and old_type != new_type: diff --git a/docs/releases/1.7.4.txt b/docs/releases/1.7.4.txt index 8fbac815bf8..5f10e5e8c60 100644 --- a/docs/releases/1.7.4.txt +++ b/docs/releases/1.7.4.txt @@ -14,3 +14,6 @@ Bugfixes * Made the migration's ``RenameModel`` operation rename ``ManyToManyField`` tables (:ticket:`24135`). + +* Fixed a migration crash on MySQL when migrating from a ``OneToOneField`` to a + ``ForeignKey`` (:ticket:`24163`). diff --git a/tests/schema/models.py b/tests/schema/models.py index df8a1c5ce4d..6eba7ccae11 100644 --- a/tests/schema/models.py +++ b/tests/schema/models.py @@ -67,6 +67,16 @@ class BookWeak(models.Model): apps = new_apps +class BookWithO2O(models.Model): + author = models.OneToOneField(Author) + title = models.CharField(max_length=100, db_index=True) + pub_date = models.DateTimeField() + + class Meta: + apps = new_apps + db_table = "schema_book" + + class BookWithM2M(models.Model): author = models.ForeignKey(Author) title = models.CharField(max_length=100, db_index=True) diff --git a/tests/schema/tests.py b/tests/schema/tests.py index b10ad07251b..c62cf7c773d 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -5,12 +5,12 @@ from django.test import TransactionTestCase from django.db import connection, DatabaseError, IntegrityError, OperationalError from django.db.models.fields import (BinaryField, BooleanField, CharField, IntegerField, PositiveIntegerField, SlugField, TextField) -from django.db.models.fields.related import ManyToManyField, ForeignKey +from django.db.models.fields.related import ForeignKey, ManyToManyField, OneToOneField from django.db.transaction import atomic from .models import (Author, AuthorWithDefaultHeight, AuthorWithM2M, Book, BookWithLongName, BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename, UniqueTest, Thing, TagThrough, BookWithM2MThrough, AuthorTag, AuthorWithM2MThrough, - AuthorWithEvenLongerName, BookWeak, Note) + AuthorWithEvenLongerName, BookWeak, Note, BookWithO2O) class SchemaTests(TransactionTestCase): @@ -28,7 +28,7 @@ class SchemaTests(TransactionTestCase): Author, AuthorWithM2M, Book, BookWithLongName, BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename, UniqueTest, Thing, TagThrough, BookWithM2MThrough, AuthorWithEvenLongerName, - BookWeak, + BookWeak, BookWithO2O, ] # Utility functions @@ -528,6 +528,106 @@ class SchemaTests(TransactionTestCase): else: self.fail("No FK constraint for author_id found") + @unittest.skipUnless(connection.features.supports_foreign_keys, "No FK support") + def test_alter_o2o_to_fk(self): + """ + #24163 - Tests altering of OneToOne to FK + """ + # Create the table + with connection.schema_editor() as editor: + editor.create_model(Author) + editor.create_model(BookWithO2O) + # Ensure the field is right to begin with + columns = self.column_classes(BookWithO2O) + self.assertEqual(columns['author_id'][0], "IntegerField") + # Make sure the FK and unique constraints are present + constraints = self.get_constraints(BookWithO2O._meta.db_table) + author_is_fk = False + author_is_unique = False + for name, details in constraints.items(): + if details['columns'] == ['author_id']: + if details['foreign_key'] and details['foreign_key'] == ('schema_author', 'id'): + author_is_fk = True + if details['unique']: + author_is_unique = True + self.assertTrue(author_is_fk, "No FK constraint for author_id found") + self.assertTrue(author_is_unique, "No unique constraint for author_id found") + # Alter the O2O to FK + new_field = ForeignKey(Author) + new_field.set_attributes_from_name("author") + with connection.schema_editor() as editor: + editor.alter_field( + BookWithO2O, + BookWithO2O._meta.get_field("author"), + new_field, + strict=True, + ) + # Ensure the field is right afterwards + columns = self.column_classes(Book) + self.assertEqual(columns['author_id'][0], "IntegerField") + # Make sure the FK constraint is present and unique constraint is absent + constraints = self.get_constraints(Book._meta.db_table) + author_is_fk = False + author_is_unique = True + for name, details in constraints.items(): + if details['columns'] == ['author_id']: + if details['foreign_key'] and details['foreign_key'] == ('schema_author', 'id'): + author_is_fk = True + if not details['unique']: + author_is_unique = False + self.assertTrue(author_is_fk, "No FK constraint for author_id found") + self.assertFalse(author_is_unique, "Unique constraint for author_id found") + + @unittest.skipUnless(connection.features.supports_foreign_keys, "No FK support") + def test_alter_fk_to_o2o(self): + """ + #24163 - Tests altering of FK to OneToOne + """ + # Create the table + with connection.schema_editor() as editor: + editor.create_model(Author) + editor.create_model(Book) + # Ensure the field is right to begin with + columns = self.column_classes(Book) + self.assertEqual(columns['author_id'][0], "IntegerField") + # Make sure the FK constraint is present and unique constraint is absent + constraints = self.get_constraints(Book._meta.db_table) + author_is_fk = False + author_is_unique = True + for name, details in constraints.items(): + if details['columns'] == ['author_id']: + if details['foreign_key'] and details['foreign_key'] == ('schema_author', 'id'): + author_is_fk = True + if not details['unique']: + author_is_unique = False + self.assertTrue(author_is_fk, "No FK constraint for author_id found") + self.assertFalse(author_is_unique, "Unique constraint for author_id found") + # Alter the O2O to FK + new_field = OneToOneField(Author) + new_field.set_attributes_from_name("author") + with connection.schema_editor() as editor: + editor.alter_field( + Book, + Book._meta.get_field("author"), + new_field, + strict=True, + ) + # Ensure the field is right afterwards + columns = self.column_classes(BookWithO2O) + self.assertEqual(columns['author_id'][0], "IntegerField") + # Make sure the FK and unique constraints are present + constraints = self.get_constraints(BookWithO2O._meta.db_table) + author_is_fk = False + author_is_unique = False + for name, details in constraints.items(): + if details['columns'] == ['author_id']: + if details['foreign_key'] and details['foreign_key'] == ('schema_author', 'id'): + author_is_fk = True + if details['unique']: + author_is_unique = True + self.assertTrue(author_is_fk, "No FK constraint for author_id found") + self.assertTrue(author_is_unique, "No unique constraint for author_id found") + def test_alter_implicit_id_to_explicit(self): """ Should be able to convert an implicit "id" field to an explicit "id"