I think this behaviour is a bug in Django's object-relationship mapping. If you look at the SQL that Django generates for your query, then you'll see something like this:
>>> q1 = (Products.objects.annotate(num_ratings = Count('ratingentries'))
... .filter(num_ratings__gt = 10))
>>> q2 = (Products.objects.annotate(num_ratings = Count('ratingentries'))
... .exclude(num_ratings__gt = 10))
>>> print(str((q1 | q2).query))
SELECT `myapp_products`.`id`, COUNT(`myapp_ratingentries`.`id`) AS
`num_ratings` FROM `myapp_products` LEFT OUTER JOIN `myapp_ratingentries` ON
(`myapp_products`.`id` = `myapp_ratingentries`.`product_id`) GROUP BY
`myapp_products`.`id` HAVING COUNT(`myapp_ratingentries`.`id`) > 10
ORDER BY NULL
Note that the condition from q1
is included in the HAVING
clause of the query, but the condition from q2
has been lost.
You can work around the problem by building your query like this:
>>> q = Q(num_products__gt = 10) | ~Q(num_products__gt = 10)
>>> q3 = Products.objects.annotate(num_ratings = Count('ratingentries')).filter(q)
>>> print(str(q3.query))
SELECT `myapp_products`.`id`, COUNT(`myapp_ratingentries`.`id`) AS
`num_ratings` FROM `myapp_products` LEFT OUTER JOIN `myapp_ratingentries` ON
(`myapp_products`.`id` = `myapp_ratingentries`.`product_id`) GROUP BY
`myapp_products`.`id` HAVING (COUNT(`myapp_ratingentries`.`id`) > 10 OR NOT
(COUNT(`myapp_ratingentries`.`id`) > 10 )) ORDER BY NULL
Note that both conditions are now included in the HAVING
clause.
I suggest that you report this to the Django developers as a bug. (If it can't be fixed, then at least it should be documented.)