From 0b9428c88dc4980df1c4d6765b58f013ff9376d0 Mon Sep 17 00:00:00 2001 From: Jib Date: Sun, 23 Mar 2025 17:20:07 -0400 Subject: [PATCH] [Bugfix]: Define new column and model instance per result --- django_mongodb_backend/queryset.py | 62 +++++++++++++------------- tests/raw_query_/test_raw_aggregate.py | 25 +++++++++++ 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/django_mongodb_backend/queryset.py b/django_mongodb_backend/queryset.py index c8b351e3..7f039572 100644 --- a/django_mongodb_backend/queryset.py +++ b/django_mongodb_backend/queryset.py @@ -53,37 +53,36 @@ def __iter__(self): compiler = connection.ops.compiler("SQLCompiler")(query, connection, db) query_iterator = iter(query) try: - # Get the columns from the first result. - try: - first_result = next(query_iterator) - except StopIteration: - # No results. - return - self.queryset.columns = list(first_result.keys()) - # Reset the iterator to include the first item. - query_iterator = self._make_result(chain([first_result], query_iterator)) - ( - model_init_names, - model_init_pos, - annotation_fields, - ) = self.queryset.resolve_model_init_order() - model_cls = self.queryset.model - if model_cls._meta.pk.attname not in model_init_names: - raise FieldDoesNotExist("Raw query must include the primary key") - fields = [self.queryset.model_fields.get(c) for c in self.queryset.columns] - converters = compiler.get_converters( - [f.get_col(f.model._meta.db_table) if f else None for f in fields] - ) - if converters: - query_iterator = compiler.apply_converters(query_iterator, converters) - for values in query_iterator: + # Get the columns for each result + for result in query_iterator: # Associate fields to values - model_init_values = [values[pos] for pos in model_init_pos] - instance = model_cls.from_db(db, model_init_names, model_init_values) - if annotation_fields: - for column, pos in annotation_fields: - setattr(instance, column, values[pos]) - yield instance + self.queryset.columns = list(result.keys()) + # Use the new columns to define the new model_init_order. + ( + model_init_names, + model_init_pos, + annotation_fields, + ) = self.queryset.resolve_model_init_order() + model_cls = self.queryset.model + if model_cls._meta.pk.attname not in model_init_names: + raise FieldDoesNotExist("Raw query must include the primary key") + fields = [self.queryset.model_fields.get(c) for c in self.queryset.columns] + converters = compiler.get_converters( + [f.get_col(f.model._meta.db_table) if f else None for f in fields] + ) + # Make an iterator from the singular result + result_iter = self._make_result([result]) + if converters: + result_iter = compiler.apply_converters(result_iter, converters) + + # Iterate once to generate a model object based solely on the result + for values in result_iter: + model_init_values = [values[pos] for pos in model_init_pos] + instance = model_cls.from_db(db, model_init_names, model_init_values) + if annotation_fields: + for column, pos in annotation_fields: + setattr(instance, column, values[pos]) + yield instance finally: query.cursor.close() @@ -93,4 +92,5 @@ def _make_result(self, query): of __iter__(). """ for result in query: - yield tuple(result.values()) + # Create a tuple of values strictly from the outlined result columns + yield tuple(result.get(key, None) for key in self.queryset.columns) diff --git a/tests/raw_query_/test_raw_aggregate.py b/tests/raw_query_/test_raw_aggregate.py index 91e844e6..9ab08e47 100644 --- a/tests/raw_query_/test_raw_aggregate.py +++ b/tests/raw_query_/test_raw_aggregate.py @@ -4,6 +4,7 @@ from datetime import date +from django.db import connection from django.core.exceptions import FieldDoesNotExist from django.test import TestCase @@ -170,6 +171,30 @@ def test_order_handler(self): authors = Author.objects.all() self.assertSuccessfulRawQuery(Author, query, authors) + def test_different_ordered_in_database(self): + """Documents in MongoDB are not required to maintain key order as + a means to improve write efficiency. Documents can be returned + to Django out of order. This can lead to incorrect information being placed + in a RawQueryset object. + """ + database = connection[""].database + raw_insert = Author(first_name="Out of", last_name="Order", dob=date(1950, 9, 20)) + try: + # Insert a document into the database in reverse + database[Author._meta.db_table].insert_one( + { + field.name: getattr(field.name, raw_insert) + for field in reversed(Author._meta.get_fields()) + } + ) + query = [] + authors = Author.objects.all() + self.assertSuccessfulRawQuery(Author, query, authors) + finally: + database[Author._meta.db_table].delete_one( + {"first_name": raw_insert.first_name, "last_name": raw_insert.last_name} + ) + def test_query_representation(self): """Test representation of raw query.""" query = [{"$match": {"last_name": "foo"}}]