Skip to content

Commit 5792e6a

Browse files
committed
Fixed #24163 -- Removed unique constraint after index on MySQL
Thanks Łukasz Harasimowicz for the report.
1 parent 8e435a5 commit 5792e6a

File tree

4 files changed

+128
-15
lines changed

4 files changed

+128
-15
lines changed

django/db/backends/base/schema.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -488,18 +488,6 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
488488
old_db_params, new_db_params, strict=False):
489489
"""Actually perform a "physical" (non-ManyToMany) field update."""
490490

491-
# Has unique been removed?
492-
if old_field.unique and (not new_field.unique or (not old_field.primary_key and new_field.primary_key)):
493-
# Find the unique constraint for this field
494-
constraint_names = self._constraint_names(model, [old_field.column], unique=True)
495-
if strict and len(constraint_names) != 1:
496-
raise ValueError("Found wrong number (%s) of unique constraints for %s.%s" % (
497-
len(constraint_names),
498-
model._meta.db_table,
499-
old_field.column,
500-
))
501-
for constraint_name in constraint_names:
502-
self.execute(self._delete_constraint_sql(self.sql_delete_unique, model, constraint_name))
503491
# Drop any FK constraints, we'll remake them later
504492
fks_dropped = set()
505493
if old_field.rel and old_field.db_constraint:
@@ -513,6 +501,18 @@ def _alter_field(self, model, old_field, new_field, old_type, new_type,
513501
for fk_name in fk_names:
514502
fks_dropped.add((old_field.column,))
515503
self.execute(self._delete_constraint_sql(self.sql_delete_fk, model, fk_name))
504+
# Has unique been removed?
505+
if old_field.unique and (not new_field.unique or (not old_field.primary_key and new_field.primary_key)):
506+
# Find the unique constraint for this field
507+
constraint_names = self._constraint_names(model, [old_field.column], unique=True)
508+
if strict and len(constraint_names) != 1:
509+
raise ValueError("Found wrong number (%s) of unique constraints for %s.%s" % (
510+
len(constraint_names),
511+
model._meta.db_table,
512+
old_field.column,
513+
))
514+
for constraint_name in constraint_names:
515+
self.execute(self._delete_constraint_sql(self.sql_delete_unique, model, constraint_name))
516516
# Drop incoming FK constraints if we're a primary key and things are going
517517
# to change.
518518
if old_field.primary_key and new_field.primary_key and old_type != new_type:

docs/releases/1.7.4.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,6 @@ Bugfixes
1414

1515
* Made the migration's ``RenameModel`` operation rename ``ManyToManyField``
1616
tables (:ticket:`24135`).
17+
18+
* Fixed a migration crash on MySQL when migrating from a ``OneToOneField`` to a
19+
``ForeignKey`` (:ticket:`24163`).

tests/schema/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ class Meta:
6767
apps = new_apps
6868

6969

70+
class BookWithO2O(models.Model):
71+
author = models.OneToOneField(Author)
72+
title = models.CharField(max_length=100, db_index=True)
73+
pub_date = models.DateTimeField()
74+
75+
class Meta:
76+
apps = new_apps
77+
db_table = "schema_book"
78+
79+
7080
class BookWithM2M(models.Model):
7181
author = models.ForeignKey(Author)
7282
title = models.CharField(max_length=100, db_index=True)

tests/schema/tests.py

Lines changed: 103 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
from django.db import connection, DatabaseError, IntegrityError, OperationalError
66
from django.db.models.fields import (BinaryField, BooleanField, CharField, IntegerField,
77
PositiveIntegerField, SlugField, TextField)
8-
from django.db.models.fields.related import ManyToManyField, ForeignKey
8+
from django.db.models.fields.related import ForeignKey, ManyToManyField, OneToOneField
99
from django.db.transaction import atomic
1010
from .models import (Author, AuthorWithDefaultHeight, AuthorWithM2M, Book, BookWithLongName,
1111
BookWithSlug, BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename,
1212
UniqueTest, Thing, TagThrough, BookWithM2MThrough, AuthorTag, AuthorWithM2MThrough,
13-
AuthorWithEvenLongerName, BookWeak, Note)
13+
AuthorWithEvenLongerName, BookWeak, Note, BookWithO2O)
1414

1515

1616
class SchemaTests(TransactionTestCase):
@@ -28,7 +28,7 @@ class SchemaTests(TransactionTestCase):
2828
Author, AuthorWithM2M, Book, BookWithLongName, BookWithSlug,
2929
BookWithM2M, Tag, TagIndexed, TagM2MTest, TagUniqueRename, UniqueTest,
3030
Thing, TagThrough, BookWithM2MThrough, AuthorWithEvenLongerName,
31-
BookWeak,
31+
BookWeak, BookWithO2O,
3232
]
3333

3434
# Utility functions
@@ -528,6 +528,106 @@ def test_alter_fk(self):
528528
else:
529529
self.fail("No FK constraint for author_id found")
530530

531+
@unittest.skipUnless(connection.features.supports_foreign_keys, "No FK support")
532+
def test_alter_o2o_to_fk(self):
533+
"""
534+
#24163 - Tests altering of OneToOne to FK
535+
"""
536+
# Create the table
537+
with connection.schema_editor() as editor:
538+
editor.create_model(Author)
539+
editor.create_model(BookWithO2O)
540+
# Ensure the field is right to begin with
541+
columns = self.column_classes(BookWithO2O)
542+
self.assertEqual(columns['author_id'][0], "IntegerField")
543+
# Make sure the FK and unique constraints are present
544+
constraints = self.get_constraints(BookWithO2O._meta.db_table)
545+
author_is_fk = False
546+
author_is_unique = False
547+
for name, details in constraints.items():
548+
if details['columns'] == ['author_id']:
549+
if details['foreign_key'] and details['foreign_key'] == ('schema_author', 'id'):
550+
author_is_fk = True
551+
if details['unique']:
552+
author_is_unique = True
553+
self.assertTrue(author_is_fk, "No FK constraint for author_id found")
554+
self.assertTrue(author_is_unique, "No unique constraint for author_id found")
555+
# Alter the O2O to FK
556+
new_field = ForeignKey(Author)
557+
new_field.set_attributes_from_name("author")
558+
with connection.schema_editor() as editor:
559+
editor.alter_field(
560+
BookWithO2O,
561+
BookWithO2O._meta.get_field("author"),
562+
new_field,
563+
strict=True,
564+
)
565+
# Ensure the field is right afterwards
566+
columns = self.column_classes(Book)
567+
self.assertEqual(columns['author_id'][0], "IntegerField")
568+
# Make sure the FK constraint is present and unique constraint is absent
569+
constraints = self.get_constraints(Book._meta.db_table)
570+
author_is_fk = False
571+
author_is_unique = True
572+
for name, details in constraints.items():
573+
if details['columns'] == ['author_id']:
574+
if details['foreign_key'] and details['foreign_key'] == ('schema_author', 'id'):
575+
author_is_fk = True
576+
if not details['unique']:
577+
author_is_unique = False
578+
self.assertTrue(author_is_fk, "No FK constraint for author_id found")
579+
self.assertFalse(author_is_unique, "Unique constraint for author_id found")
580+
581+
@unittest.skipUnless(connection.features.supports_foreign_keys, "No FK support")
582+
def test_alter_fk_to_o2o(self):
583+
"""
584+
#24163 - Tests altering of FK to OneToOne
585+
"""
586+
# Create the table
587+
with connection.schema_editor() as editor:
588+
editor.create_model(Author)
589+
editor.create_model(Book)
590+
# Ensure the field is right to begin with
591+
columns = self.column_classes(Book)
592+
self.assertEqual(columns['author_id'][0], "IntegerField")
593+
# Make sure the FK constraint is present and unique constraint is absent
594+
constraints = self.get_constraints(Book._meta.db_table)
595+
author_is_fk = False
596+
author_is_unique = True
597+
for name, details in constraints.items():
598+
if details['columns'] == ['author_id']:
599+
if details['foreign_key'] and details['foreign_key'] == ('schema_author', 'id'):
600+
author_is_fk = True
601+
if not details['unique']:
602+
author_is_unique = False
603+
self.assertTrue(author_is_fk, "No FK constraint for author_id found")
604+
self.assertFalse(author_is_unique, "Unique constraint for author_id found")
605+
# Alter the O2O to FK
606+
new_field = OneToOneField(Author)
607+
new_field.set_attributes_from_name("author")
608+
with connection.schema_editor() as editor:
609+
editor.alter_field(
610+
Book,
611+
Book._meta.get_field("author"),
612+
new_field,
613+
strict=True,
614+
)
615+
# Ensure the field is right afterwards
616+
columns = self.column_classes(BookWithO2O)
617+
self.assertEqual(columns['author_id'][0], "IntegerField")
618+
# Make sure the FK and unique constraints are present
619+
constraints = self.get_constraints(BookWithO2O._meta.db_table)
620+
author_is_fk = False
621+
author_is_unique = False
622+
for name, details in constraints.items():
623+
if details['columns'] == ['author_id']:
624+
if details['foreign_key'] and details['foreign_key'] == ('schema_author', 'id'):
625+
author_is_fk = True
626+
if details['unique']:
627+
author_is_unique = True
628+
self.assertTrue(author_is_fk, "No FK constraint for author_id found")
629+
self.assertTrue(author_is_unique, "No unique constraint for author_id found")
630+
531631
def test_alter_implicit_id_to_explicit(self):
532632
"""
533633
Should be able to convert an implicit "id" field to an explicit "id"

0 commit comments

Comments
 (0)