From 0bce2f102c5734d0c7ff1ebf0b10a316d58ed5ce Mon Sep 17 00:00:00 2001
From: Berker Peksag <berker.peksag@gmail.com>
Date: Fri, 3 Jun 2016 12:55:30 -0700
Subject: [PATCH] Fixed #12810 -- Added a check for clashing
 ManyToManyField.db_table names.

---
 django/db/models/fields/related.py        | 31 +++++++++++
 docs/ref/checks.txt                       |  2 +
 tests/invalid_models_tests/test_models.py | 66 +++++++++++++++++++++++
 3 files changed, 99 insertions(+)

diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
index 4b8b1804b5..0639bf3d79 100644
--- a/django/db/models/fields/related.py
+++ b/django/db/models/fields/related.py
@@ -1194,6 +1194,7 @@ class ManyToManyField(RelatedField):
         errors.extend(self._check_unique(**kwargs))
         errors.extend(self._check_relationship_model(**kwargs))
         errors.extend(self._check_ignored_options(**kwargs))
+        errors.extend(self._check_table_uniqueness(**kwargs))
         return errors
 
     def _check_unique(self, **kwargs):
@@ -1429,6 +1430,36 @@ class ManyToManyField(RelatedField):
 
         return errors
 
+    def _check_table_uniqueness(self, **kwargs):
+        if isinstance(self.remote_field.through, six.string_types):
+            return []
+        registered_tables = {
+            model._meta.db_table: model
+            for model in self.opts.apps.get_models(include_auto_created=True)
+            if model != self.remote_field.through
+        }
+        m2m_db_table = self.m2m_db_table()
+        if m2m_db_table in registered_tables:
+            model = registered_tables[m2m_db_table]
+            if model._meta.auto_created:
+                def _get_field_name(model):
+                    for field in model._meta.auto_created._meta.many_to_many:
+                        if field.remote_field.through is model:
+                            return field.name
+                opts = model._meta.auto_created._meta
+                clashing_obj = '%s.%s' % (opts.label, _get_field_name(model))
+            else:
+                clashing_obj = '%s' % model._meta.label
+            return [
+                checks.Error(
+                    "The field's intermediary table '%s' clashes with the "
+                    "table name of '%s'." % (m2m_db_table, clashing_obj),
+                    obj=self,
+                    id='fields.E340',
+                )
+            ]
+        return []
+
     def deconstruct(self):
         name, path, args, kwargs = super(ManyToManyField, self).deconstruct()
         # Handle the simpler arguments.
diff --git a/docs/ref/checks.txt b/docs/ref/checks.txt
index 074a901de8..aff3bd2bdc 100644
--- a/docs/ref/checks.txt
+++ b/docs/ref/checks.txt
@@ -245,6 +245,8 @@ Related Fields
 * **fields.E338**: The intermediary model ``<through model>`` has no field
   ``<field name>``.
 * **fields.E339**: ``<model>.<field name>`` is not a foreign key to ``<model>``.
+* **fields.E340**: The field's intermediary table ``<table name>`` clashes with
+  the table name of ``<model>``/``<model>.<field name>``.
 * **fields.W340**: ``null`` has no effect on ``ManyToManyField``.
 * **fields.W341**: ``ManyToManyField`` does not support ``validators``.
 * **fields.W342**: Setting ``unique=True`` on a ``ForeignKey`` has the same
diff --git a/tests/invalid_models_tests/test_models.py b/tests/invalid_models_tests/test_models.py
index b8f15cae60..fa63b9e8a7 100644
--- a/tests/invalid_models_tests/test_models.py
+++ b/tests/invalid_models_tests/test_models.py
@@ -761,3 +761,69 @@ class OtherModelTests(SimpleTestCase):
             'as an implicit link is deprecated.'
         )
         self.assertEqual(ParkingLot._meta.pk.name, 'parent')
+
+    def test_m2m_table_name_clash(self):
+        class Foo(models.Model):
+            bar = models.ManyToManyField('Bar', db_table='myapp_bar')
+
+            class Meta:
+                db_table = 'myapp_foo'
+
+        class Bar(models.Model):
+            class Meta:
+                db_table = 'myapp_bar'
+
+        self.assertEqual(Foo.check(), [
+            Error(
+                "The field's intermediary table 'myapp_bar' clashes with the "
+                "table name of 'invalid_models_tests.Bar'.",
+                obj=Foo._meta.get_field('bar'),
+                id='fields.E340',
+            )
+        ])
+
+    def test_m2m_field_table_name_clash(self):
+        class Foo(models.Model):
+            pass
+
+        class Bar(models.Model):
+            foos = models.ManyToManyField(Foo, db_table='clash')
+
+        class Baz(models.Model):
+            foos = models.ManyToManyField(Foo, db_table='clash')
+
+        self.assertEqual(Bar.check() + Baz.check(), [
+            Error(
+                "The field's intermediary table 'clash' clashes with the "
+                "table name of 'invalid_models_tests.Baz.foos'.",
+                obj=Bar._meta.get_field('foos'),
+                id='fields.E340',
+            ),
+            Error(
+                "The field's intermediary table 'clash' clashes with the "
+                "table name of 'invalid_models_tests.Bar.foos'.",
+                obj=Baz._meta.get_field('foos'),
+                id='fields.E340',
+            )
+        ])
+
+    def test_m2m_autogenerated_table_name_clash(self):
+        class Foo(models.Model):
+            class Meta:
+                db_table = 'bar_foos'
+
+        class Bar(models.Model):
+            # The autogenerated `db_table` will be bar_foos.
+            foos = models.ManyToManyField(Foo)
+
+            class Meta:
+                db_table = 'bar'
+
+        self.assertEqual(Bar.check(), [
+            Error(
+                "The field's intermediary table 'bar_foos' clashes with the "
+                "table name of 'invalid_models_tests.Foo'.",
+                obj=Bar._meta.get_field('foos'),
+                id='fields.E340',
+            )
+        ])