Skip to content

Commit fba32d9

Browse files
committed
add clashing field name validation
1 parent c866e2e commit fba32d9

File tree

4 files changed

+97
-5
lines changed

4 files changed

+97
-5
lines changed

django_mongodb_backend/fields/polymorphic_embedded_model.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
from django.core import checks
55
from django.core.exceptions import FieldDoesNotExist, ValidationError
6-
from django.db import models
6+
from django.db import connections, models
77
from django.db.models.fields.related import lazy_related_operation
88
from django.db.models.lookups import Transform
99

@@ -33,6 +33,7 @@ def check(self, **kwargs):
3333
from ..models import EmbeddedModel
3434

3535
errors = super().check(**kwargs)
36+
embedded_fields = {}
3637
for model in self.embedded_models:
3738
if not issubclass(model, EmbeddedModel):
3839
return [
@@ -55,6 +56,23 @@ def check(self, **kwargs):
5556
id="django_mongodb_backend.embedded_model.E001",
5657
)
5758
)
59+
field_name = field.name
60+
if existing_field := embedded_fields.get(field.name):
61+
connection = _get_mongodb_connection()
62+
if existing_field.db_type(connection) != field.db_type(connection):
63+
errors.append(
64+
checks.Warning(
65+
f"Embedded models {existing_field.model._meta.label} "
66+
f"and {field.model._meta.label} both have field "
67+
f"'{field_name}' of different type.",
68+
obj=self,
69+
id="django_mongodb_backend.embedded_model.E003",
70+
hint="It may be impossible to query both fields.",
71+
)
72+
)
73+
74+
else:
75+
embedded_fields[field_name] = field
5876
return errors
5977

6078
def deconstruct(self):
@@ -222,3 +240,10 @@ def __init__(self, key_name, ref_field):
222240

223241
def __call__(self, *args, **kwargs):
224242
return KeyTransform(self.key_name, self.ref_field, *args, **kwargs)
243+
244+
245+
def _get_mongodb_connection():
246+
for alias in connections:
247+
if connections[alias].vendor == "mongodb":
248+
return connections[alias]
249+
return None

docs/source/ref/models/fields.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ These indexes use 0-based indexing.
321321

322322
.. versionadded:: 5.2.0b2
323323

324-
Stores a model of type ``embedded_models``.
324+
Stores a model of one of the types in ``embedded_models``.
325325

326326
.. attribute:: embedded_models
327327

@@ -337,7 +337,8 @@ These indexes use 0-based indexing.
337337
.. admonition:: Migrations support is limited
338338

339339
:djadmin:`makemigrations` does not yet detect changes to embedded models,
340-
nor does it create indexes or constraints for embedded models.
340+
nor does it create indexes or constraints for embedded models referenced
341+
by ``PolymorphicEmbeddedModelField``.
341342

342343
.. admonition:: Forms are not supported
343344

docs/source/topics/embedded-models.rst

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,8 +246,8 @@ Represented in BSON, the person structures looks like this:
246246
pet: { name: 'Pheobe', purrs: true, _label: 'myapp.Cat' }
247247
}
248248
249-
The ``_label`` field contains the model's
250-
:attr:`~django.db.models.Options.label`.
249+
The ``_label`` field tracks the model's :attr:`~django.db.models.Options.label`
250+
so that the model can be initialized properly.
251251

252252
Querying ``PolymorphicEmbeddedModelField``
253253
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -262,3 +262,31 @@ You can also filter on fields that aren't shared among the embedded models. For
262262
example, if you filter on ``barks``, you'll only get back people with dogs::
263263

264264
>>> Person.objects.filter(pet__barks=True)
265+
266+
Clashing field names
267+
~~~~~~~~~~~~~~~~~~~~
268+
269+
Be careful not to use embedded models with clashing field names of different
270+
types. For example::
271+
272+
from django.db import models
273+
274+
from django_mongodb_backend.fields import PolymorphicEmbeddedModelField
275+
from django_mongodb_backend.models import EmbeddedModel
276+
277+
class Target1(EmbeddedModel):
278+
number = models.IntegerField()
279+
280+
class Target2(EmbeddedModel):
281+
number = models.DecimalField(max_digits=4, decimal_places=2)
282+
283+
class Example(models.Model):
284+
target = PolymorphicEmbeddedModelField([Target1, Target2])
285+
286+
In this case, it will be impossible to query the ``number`` field properly
287+
since Django won't know whether to prepare the lookup value as an integer or as
288+
a decimal.
289+
290+
Additionally, querying into nested embedded model fields with the same name
291+
isn't well supported: the first model in ``embedded_models`` is the one that
292+
will be used for lookups.

tests/model_fields_/test_polymorphic_embedded_model.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,3 +211,41 @@ class MyModel(models.Model):
211211
msg,
212212
"Embedded models must be a subclass of django_mongodb_backend.models.EmbeddedModel.",
213213
)
214+
215+
def test_clashing_fields(self):
216+
class Target1(EmbeddedModel):
217+
clash = models.DecimalField(max_digits=4, decimal_places=2)
218+
219+
class Target2(EmbeddedModel):
220+
clash = models.CharField(max_length=255)
221+
222+
class MyModel(models.Model):
223+
field = PolymorphicEmbeddedModelField([Target1, Target2])
224+
225+
errors = MyModel().check()
226+
self.assertEqual(len(errors), 1)
227+
self.assertEqual(errors[0].id, "django_mongodb_backend.embedded_model.E003")
228+
self.assertEqual(
229+
errors[0].msg,
230+
"Embedded models model_fields_.Target1 and model_fields_.Target2 "
231+
"both have field 'clash' of different type.",
232+
)
233+
self.assertEqual(
234+
errors[0].hint,
235+
"It may be impossible to query both fields.",
236+
)
237+
238+
def test_clashing_fields_of_same_type(self):
239+
"""Fields of different type don't clash if they use the same db_type."""
240+
241+
class Target1(EmbeddedModel):
242+
clash = models.TextField()
243+
244+
class Target2(EmbeddedModel):
245+
clash = models.CharField(max_length=255)
246+
247+
class MyModel(models.Model):
248+
field = PolymorphicEmbeddedModelField([Target1, Target2])
249+
250+
errors = MyModel().check()
251+
self.assertEqual(len(errors), 0)

0 commit comments

Comments
 (0)