From 781e710e9fb2c6665448208819b09f593d3dbd9f Mon Sep 17 00:00:00 2001 From: Emanuel Lupi Date: Tue, 21 Oct 2025 00:48:17 -0300 Subject: [PATCH] INTPYTHON-804 Prevent QuerySet.union() queries from duplicating $project --- django_mongodb_backend/compiler.py | 5 +- docs/releases/5.2.x.rst | 3 +- tests/queries_/models.py | 1 + tests/queries_/test_qs_combinators.py | 71 +++++++++++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) create mode 100644 tests/queries_/test_qs_combinators.py diff --git a/django_mongodb_backend/compiler.py b/django_mongodb_backend/compiler.py index 1c20faa18..e908cba32 100644 --- a/django_mongodb_backend/compiler.py +++ b/django_mongodb_backend/compiler.py @@ -625,7 +625,10 @@ def get_combinator_queries(self): fields[expr.alias] = 1 else: fields[alias] = f"${ref}" if alias != ref else 1 - inner_pipeline.append({"$project": fields}) + # Avoid duplicating the same $project stage when reusing subquery + # projections. + if not inner_pipeline or inner_pipeline[-1] != {"$project": fields}: + inner_pipeline.append({"$project": fields}) # Combine query with the current combinator pipeline. if combinator_pipeline: combinator_pipeline.append( diff --git a/docs/releases/5.2.x.rst b/docs/releases/5.2.x.rst index d607c81a0..bed6a6cac 100644 --- a/docs/releases/5.2.x.rst +++ b/docs/releases/5.2.x.rst @@ -15,7 +15,8 @@ New features Bug fixes --------- -- ... +- Prevented ``QuerySet.union()`` queries from duplicating the ``$project`` + pipeline. 5.2.2 ===== diff --git a/tests/queries_/models.py b/tests/queries_/models.py index 2e56ebc64..f1dd650b0 100644 --- a/tests/queries_/models.py +++ b/tests/queries_/models.py @@ -13,6 +13,7 @@ def __str__(self): class Book(models.Model): title = models.CharField(max_length=10) author = models.ForeignKey(Author, models.CASCADE) + isbn = models.CharField(max_length=13) def __str__(self): return self.title diff --git a/tests/queries_/test_qs_combinators.py b/tests/queries_/test_qs_combinators.py new file mode 100644 index 000000000..360fcbc84 --- /dev/null +++ b/tests/queries_/test_qs_combinators.py @@ -0,0 +1,71 @@ +from django.test import TestCase + +from django_mongodb_backend.test import MongoTestCaseMixin + +from .models import Book + + +class UnionTests(MongoTestCaseMixin, TestCase): + def test_union_simple_conditions(self): + with self.assertNumQueries(1) as ctx: + list(Book.objects.filter(title="star wars").union(Book.objects.filter(isbn__in="1234"))) + self.assertAggregateQuery( + ctx.captured_queries[0]["sql"], + "queries__book", + [ + {"$match": {"title": "star wars"}}, + {"$project": {"_id": 1, "author_id": 1, "title": 1, "isbn": 1}}, + { + "$unionWith": { + "coll": "queries__book", + "pipeline": [ + {"$match": {"isbn": {"$in": ("1", "2", "3", "4")}}}, + {"$project": {"_id": 1, "author_id": 1, "title": 1, "isbn": 1}}, + ], + } + }, + { + "$group": { + "_id": { + "_id": "$_id", + "author_id": "$author_id", + "title": "$title", + "isbn": "$isbn", + } + } + }, + { + "$addFields": { + "_id": "$_id._id", + "author_id": "$_id.author_id", + "title": "$_id.title", + "isbn": "$_id.isbn", + } + }, + ], + ) + + def test_union_all_simple_conditions(self): + with self.assertNumQueries(1) as ctx: + list( + Book.objects.filter(title="star wars").union( + Book.objects.filter(isbn="1234"), all=True + ) + ) + self.assertAggregateQuery( + ctx.captured_queries[0]["sql"], + "queries__book", + [ + {"$match": {"title": "star wars"}}, + {"$project": {"_id": 1, "author_id": 1, "title": 1, "isbn": 1}}, + { + "$unionWith": { + "coll": "queries__book", + "pipeline": [ + {"$match": {"isbn": "1234"}}, + {"$project": {"_id": 1, "author_id": 1, "title": 1, "isbn": 1}}, + ], + } + }, + ], + )