From 2d30fdbd68eaea0a6ffc37b491e9083eaec2f8b5 Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Wed, 24 Sep 2025 19:32:22 -0400 Subject: [PATCH] INTPYTHON-765 Fix crash loading embedded models with missing fields that use database converters --- django_mongodb_backend/operations.py | 4 ++++ docs/releases/5.2.x.rst | 5 ++++- tests/model_fields_/test_embedded_model.py | 12 +++++++++++- tests/model_fields_/test_embedded_model_array.py | 12 ++++++++++++ .../model_fields_/test_polymorphic_embedded_model.py | 12 +++++++++++- .../test_polymorphic_embedded_model_array.py | 12 +++++++++++- 6 files changed, 53 insertions(+), 4 deletions(-) diff --git a/django_mongodb_backend/operations.py b/django_mongodb_backend/operations.py index 4b494c3d3..79ac030da 100644 --- a/django_mongodb_backend/operations.py +++ b/django_mongodb_backend/operations.py @@ -184,6 +184,8 @@ def convert_embeddedmodelfield_value(self, value, expression, connection): if value is not None: # Apply database converters to each field of the embedded model. for field in expression.output_field.embedded_model._meta.fields: + if field.attname not in value: + continue field_expr = Expression(output_field=field) converters = connection.ops.get_db_converters( field_expr @@ -204,6 +206,8 @@ def convert_polymorphicembeddedmodelfield_value(self, value, expression, connect model_class = expression.output_field._get_model_from_label(value["_label"]) # Apply database converters to each field of the embedded model. for field in model_class._meta.fields: + if field.attname not in value: + continue field_expr = Expression(output_field=field) converters = connection.ops.get_db_converters( field_expr diff --git a/docs/releases/5.2.x.rst b/docs/releases/5.2.x.rst index c2a559f3f..c49d87b7f 100644 --- a/docs/releases/5.2.x.rst +++ b/docs/releases/5.2.x.rst @@ -15,7 +15,10 @@ New features Bug fixes --------- -- ... +- Fixed a ``KeyError`` crash when loading models with ``EmbeddedModel`` fields + that use a database converter, if the field isn't present in the data (e.g. + data not written by Django, or after a field was added to an existing + ``EmbeddedModel``). Deprecated features ------------------- diff --git a/tests/model_fields_/test_embedded_model.py b/tests/model_fields_/test_embedded_model.py index a94090ecf..199fde1b2 100644 --- a/tests/model_fields_/test_embedded_model.py +++ b/tests/model_fields_/test_embedded_model.py @@ -2,7 +2,7 @@ from datetime import timedelta from django.core.exceptions import FieldDoesNotExist, ValidationError -from django.db import models +from django.db import connection, models from django.db.models import ( Exists, ExpressionWrapper, @@ -107,6 +107,16 @@ def test_pre_save(self): self.assertEqual(obj.data.auto_now_add, auto_now_add) self.assertGreater(obj.data.auto_now, auto_now_two) + def test_missing_field_in_data(self): + """ + Loading a model with an EmbeddedModelField that has a missing subfield + (e.g. data not written by Django) that uses a database converter (in + this case, integer is an IntegerField) doesn't crash. + """ + Holder.objects.create(data=Data(integer=5)) + connection.database.model_fields__holder.update_many({}, {"$unset": {"data.integer": ""}}) + self.assertIsNone(Holder.objects.first().data.integer) + class QueryingTests(TestCase): @classmethod diff --git a/tests/model_fields_/test_embedded_model_array.py b/tests/model_fields_/test_embedded_model_array.py index 363344f3a..381ffd5e9 100644 --- a/tests/model_fields_/test_embedded_model_array.py +++ b/tests/model_fields_/test_embedded_model_array.py @@ -62,6 +62,18 @@ def test_save_load_null(self): movie = Movie.objects.get(title="Lion King") self.assertIsNone(movie.reviews) + def test_missing_field_in_data(self): + """ + Loading a model with an EmbeddedModelArrayField that has a missing + subfield (e.g. data not written by Django) that uses a database + converter (in this case, rating is an IntegerField) doesn't crash. + """ + Movie.objects.create(title="Lion King", reviews=[Review(title="The best", rating=10)]) + connection.database.model_fields__movie.update_many( + {}, {"$unset": {"reviews.$[].rating": ""}} + ) + self.assertIsNone(Movie.objects.first().reviews[0].rating) + class QueryingTests(TestCase): @classmethod diff --git a/tests/model_fields_/test_polymorphic_embedded_model.py b/tests/model_fields_/test_polymorphic_embedded_model.py index 94b54976a..5a34d25f3 100644 --- a/tests/model_fields_/test_polymorphic_embedded_model.py +++ b/tests/model_fields_/test_polymorphic_embedded_model.py @@ -2,7 +2,7 @@ from decimal import Decimal from django.core.exceptions import FieldDoesNotExist, ValidationError -from django.db import models +from django.db import connection, models from django.test import SimpleTestCase, TestCase from django.test.utils import isolate_apps @@ -91,6 +91,16 @@ def test_pre_save(self): # simultaneously. self.assertAlmostEqual(updated_at, created_at, delta=timedelta(microseconds=1000)) + def test_missing_field_in_data(self): + """ + Loading a model with a PolymorphicEmbeddedModelField that has a missing + subfield (e.g. data not written by Django) that uses a database + converter (in this case, weight is a DecimalField) doesn't crash. + """ + Person.objects.create(pet=Cat(name="Pheobe", weight="3.5")) + connection.database.model_fields__person.update_many({}, {"$unset": {"pet.weight": ""}}) + self.assertIsNone(Person.objects.first().pet.weight) + class QueryingTests(TestCase): @classmethod diff --git a/tests/model_fields_/test_polymorphic_embedded_model_array.py b/tests/model_fields_/test_polymorphic_embedded_model_array.py index 2e9b0d623..ca357d9fc 100644 --- a/tests/model_fields_/test_polymorphic_embedded_model_array.py +++ b/tests/model_fields_/test_polymorphic_embedded_model_array.py @@ -1,7 +1,7 @@ from decimal import Decimal from django.core.exceptions import FieldDoesNotExist -from django.db import models +from django.db import connection, models from django.test import SimpleTestCase, TestCase from django.test.utils import isolate_apps @@ -62,6 +62,16 @@ def test_save_load_null(self): owner = Owner.objects.get(name="Bob") self.assertIsNone(owner.pets) + def test_missing_field_in_data(self): + """ + Loading a model with a PolymorphicEmbeddedModelArrayField that has a + missing subfield (e.g. data not written by Django) that uses a database + converter (in this case, weight is a DecimalField) doesn't crash. + """ + Owner.objects.create(name="Bob", pets=[Cat(name="Phoebe", weight="3.5")]) + connection.database.model_fields__owner.update_many({}, {"$unset": {"pets.$[].weight": ""}}) + self.assertIsNone(Owner.objects.first().pets[0].weight) + class QueryingTests(TestCase): @classmethod