Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,7 @@ answer newbie questions, and generally made Django that much better:
Wilson Miner <wminer@gmail.com>
Wim Glenn <hey@wimglenn.com>
wojtek
Xavier Francisco <xavier.n.francisco@gmail.com>
Xia Kai <https://blog.xiaket.org/>
Yann Fouillat <gagaro42@gmail.com>
Yann Malet
Expand Down
17 changes: 14 additions & 3 deletions django/db/models/fields/related.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from django import forms
from django.apps import apps
from django.conf import SettingsReference
from django.conf import SettingsReference, settings
from django.core import checks, exceptions
from django.db import connection, router
from django.db.backends import utils
Expand Down Expand Up @@ -1436,12 +1436,23 @@ def _get_field_name(model):
clashing_obj = '%s.%s' % (opts.label, _get_field_name(model))
else:
clashing_obj = model._meta.label
if settings.DATABASE_ROUTERS:
error_class, error_id = checks.Warning, 'fields.W344'
error_hint = (
'You have configured settings.DATABASE_ROUTERS. Verify '
'that the table of %r is correctly routed to a separate '
'database.' % clashing_obj
)
else:
error_class, error_id = checks.Error, 'fields.E340'
error_hint = None
return [
checks.Error(
error_class(
"The field's intermediary table '%s' clashes with the "
"table name of '%s'." % (m2m_db_table, clashing_obj),
obj=self,
id='fields.E340',
hint=error_hint,
id=error_id,
)
]
return []
Expand Down
3 changes: 2 additions & 1 deletion docs/ref/checks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ Related fields
* **fields.W342**: Setting ``unique=True`` on a ``ForeignKey`` has the same
effect as using a ``OneToOneField``.
* **fields.W343**: ``limit_choices_to`` has no effect on ``ManyToManyField``
with a ``through`` model.
* **fields.W344**: The field's intermediary table ``<table name>`` clashes with
the table name of ``<model>``/``<model>.<field name>``.

Models
------
Expand Down
83 changes: 83 additions & 0 deletions tests/invalid_models_tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
from django.test.utils import isolate_apps, override_settings, register_lookup


class EmptyRouter:
pass


def get_max_column_name_length():
allowed_len = None
db_alias = None
Expand Down Expand Up @@ -1044,6 +1048,32 @@ class Meta:
)
])

@override_settings(DATABASE_ROUTERS=['invalid_models_tests.test_models.EmptyRouter'])
def test_m2m_table_name_clash_database_routers_installed(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(), [
Warning(
"The field's intermediary table 'myapp_bar' clashes with the "
"table name of 'invalid_models_tests.Bar'.",
obj=Foo._meta.get_field('bar'),
hint=(
"You have configured settings.DATABASE_ROUTERS. Verify "
"that the table of 'invalid_models_tests.Bar' is "
"correctly routed to a separate database."
),
id='fields.W344',
),
])

def test_m2m_field_table_name_clash(self):
class Foo(models.Model):
pass
Expand All @@ -1069,6 +1099,32 @@ class Baz(models.Model):
)
])

@override_settings(DATABASE_ROUTERS=['invalid_models_tests.test_models.EmptyRouter'])
def test_m2m_field_table_name_clash_database_routers_installed(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(), [
Warning(
"The field's intermediary table 'clash' clashes with the "
"table name of 'invalid_models_tests.%s.foos'."
% clashing_model,
obj=model_cls._meta.get_field('foos'),
hint=(
"You have configured settings.DATABASE_ROUTERS. Verify "
"that the table of 'invalid_models_tests.%s.foos' is "
"correctly routed to a separate database." % clashing_model
),
id='fields.W344',
) for model_cls, clashing_model in [(Bar, 'Baz'), (Baz, 'Bar')]
])

def test_m2m_autogenerated_table_name_clash(self):
class Foo(models.Model):
class Meta:
Expand All @@ -1090,6 +1146,33 @@ class Meta:
)
])

@override_settings(DATABASE_ROUTERS=['invalid_models_tests.test_models.EmptyRouter'])
def test_m2m_autogenerated_table_name_clash_database_routers_installed(self):
class Foo(models.Model):
class Meta:
db_table = 'bar_foos'

class Bar(models.Model):
# The autogenerated db_table is bar_foos.
foos = models.ManyToManyField(Foo)

class Meta:
db_table = 'bar'

self.assertEqual(Bar.check(), [
Warning(
"The field's intermediary table 'bar_foos' clashes with the "
"table name of 'invalid_models_tests.Foo'.",
obj=Bar._meta.get_field('foos'),
hint=(
"You have configured settings.DATABASE_ROUTERS. Verify "
"that the table of 'invalid_models_tests.Foo' is "
"correctly routed to a separate database."
),
id='fields.W344',
),
])

def test_m2m_unmanaged_shadow_models_not_checked(self):
class A1(models.Model):
pass
Expand Down