From 8d645cb713e9376f10a37a27c4ac86fecf431d9d Mon Sep 17 00:00:00 2001 From: Audrius Masalskis Date: Wed, 4 Mar 2026 10:29:12 +0200 Subject: [PATCH 1/3] Fix Subquery annotation detection in dnfs() for Django 6.0+ Django 6.0 resolves Subquery annotations into raw Query objects in qs.query.annotations. The existing isinstance(q, Subquery) check misses these, causing cross-table Subquery dependencies to be invisible to cache invalidation. Co-Authored-By: Claude Opus 4.6 --- cacheops/tree.py | 11 ++++++++--- tests/tests.py | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/cacheops/tree.py b/cacheops/tree.py index 6334ac73..19d23154 100644 --- a/cacheops/tree.py +++ b/cacheops/tree.py @@ -159,9 +159,14 @@ def table_for(alias): # Add any subqueries used for annotation if qs.query.annotations: - subqueries = (query_dnf(getattr(q, 'query', None)) - for q in qs.query.annotations.values() if isinstance(q, Subquery)) - dnfs_.update(join_with(lcat, subqueries)) + sub_dnfs = [] + for q in qs.query.annotations.values(): + if isinstance(q, Subquery): + sub_dnfs.append(query_dnf(getattr(q, 'query', None))) + elif isinstance(q, Query): + sub_dnfs.append(query_dnf(q)) + if sub_dnfs: + dnfs_.update(join_with(lcat, sub_dnfs)) return dnfs_ diff --git a/tests/tests.py b/tests/tests.py index 33f35945..0279d4d1 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -643,6 +643,30 @@ def test_365(self): categories = Category.objects.cache().annotate(newest_post=Subquery(newest_post[:1])) self.assertEqual(categories[0].newest_post, post.pk) + def test_366(self): + """ + Check that dnfs() detects table dependencies from + Subquery annotations. + + Django 6.0+ resolves Subquery into raw Query objects + in qs.query.annotations, so this verifies detection + works regardless of how the annotation is stored. + """ + from cacheops.tree import dnfs + + newest_post = Post.objects.filter( + category=OuterRef('pk') + ).order_by('-pk').values('pk') + qs = Category.objects.cache().annotate( + newest_post=Subquery(newest_post[:1]) + ) + result = dnfs(qs) + + self.assertEqual(result, { + Category._meta.db_table: [{}], + Post._meta.db_table: [{}], + }) + @unittest.skipIf(platform.python_implementation() == "PyPy", "dill doesn't do that in PyPy") def test_385(self): Client.objects.create(name='Client Name') From aa68dbc310d792ad3bcf63d8656fe3f4a28bc545 Mon Sep 17 00:00:00 2001 From: Audrius Masalskis Date: Wed, 4 Mar 2026 10:56:11 +0200 Subject: [PATCH 2/3] Tweak the new test to make it more compact --- tests/tests.py | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/tests/tests.py b/tests/tests.py index 0279d4d1..f99f8e26 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -645,27 +645,15 @@ def test_365(self): def test_366(self): """ - Check that dnfs() detects table dependencies from - Subquery annotations. - - Django 6.0+ resolves Subquery into raw Query objects - in qs.query.annotations, so this verifies detection - works regardless of how the annotation is stored. + Check that dnfs() detects table dependencies from Subquery annotations. + Django 6.0+ resolves Subquery into raw Query objects in qs.query.annotations, so this + verifies detection works regardless of how the annotation is stored. """ from cacheops.tree import dnfs - newest_post = Post.objects.filter( - category=OuterRef('pk') - ).order_by('-pk').values('pk') - qs = Category.objects.cache().annotate( - newest_post=Subquery(newest_post[:1]) - ) - result = dnfs(qs) - - self.assertEqual(result, { - Category._meta.db_table: [{}], - Post._meta.db_table: [{}], - }) + newest_post = Post.objects.filter(category=OuterRef('pk')).order_by('-pk').values('pk') + qs = Category.objects.cache().annotate(newest_post=Subquery(newest_post[:1])) + self.assertEqual(dnfs(qs), {Category._meta.db_table: [{}], Post._meta.db_table: [{}]}) @unittest.skipIf(platform.python_implementation() == "PyPy", "dill doesn't do that in PyPy") def test_385(self): From 98eb0c882e0aab79d242812f0450f0beec303939 Mon Sep 17 00:00:00 2001 From: Audrius Masalskis Date: Wed, 4 Mar 2026 11:03:19 +0200 Subject: [PATCH 3/3] Address feddback --- cacheops/tree.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cacheops/tree.py b/cacheops/tree.py index 19d23154..de2638d8 100644 --- a/cacheops/tree.py +++ b/cacheops/tree.py @@ -165,8 +165,7 @@ def table_for(alias): sub_dnfs.append(query_dnf(getattr(q, 'query', None))) elif isinstance(q, Query): sub_dnfs.append(query_dnf(q)) - if sub_dnfs: - dnfs_.update(join_with(lcat, sub_dnfs)) + dnfs_.update(join_with(lcat, sub_dnfs) or {}) return dnfs_