mirror of
				https://github.com/django/django.git
				synced 2025-10-31 01:25:32 +00:00 
			
		
		
		
	[1.6.x] Fixed #21748 -- join promotion for negated AND conditions
Made sure Django treats case .filter(NOT (a AND b)) the same way as
.filter((NOT a OR NOT b)) for join promotion.
Heavily modified backpatch of 35cecb1ebd
from master.
Conflicts:
	django/db/models/sql/query.py
	tests/queries/tests.py
			
			
This commit is contained in:
		| @@ -1211,16 +1211,18 @@ class Query(object): | |||||||
|         connector = q_object.connector |         connector = q_object.connector | ||||||
|         current_negated = current_negated ^ q_object.negated |         current_negated = current_negated ^ q_object.negated | ||||||
|         branch_negated = branch_negated or q_object.negated |         branch_negated = branch_negated or q_object.negated | ||||||
|         # Note that if the connector happens to match what we have already in |  | ||||||
|         # the tree, the add will be a no-op. |  | ||||||
|         target_clause = self.where_class(connector=connector, |         target_clause = self.where_class(connector=connector, | ||||||
|                                          negated=q_object.negated) |                                          negated=q_object.negated) | ||||||
|  |         # Treat case NOT (a AND b) like case ((NOT a) OR (NOT b)) for join | ||||||
|         if connector == OR: |         # promotion. See ticket #21748. | ||||||
|  |         effective_connector = connector | ||||||
|  |         if current_negated: | ||||||
|  |             effective_connector = OR if effective_connector == AND else AND | ||||||
|  |         if effective_connector == OR: | ||||||
|             alias_usage_counts = dict() |             alias_usage_counts = dict() | ||||||
|             aliases_before = set(self.tables) |             aliases_before = set(self.tables) | ||||||
|         for child in q_object.children: |         for child in q_object.children: | ||||||
|             if connector == OR: |             if effective_connector == OR: | ||||||
|                 refcounts_before = self.alias_refcount.copy() |                 refcounts_before = self.alias_refcount.copy() | ||||||
|             if isinstance(child, Node): |             if isinstance(child, Node): | ||||||
|                 child_clause = self._add_q( |                 child_clause = self._add_q( | ||||||
| @@ -1231,11 +1233,11 @@ class Query(object): | |||||||
|                     child, can_reuse=used_aliases, branch_negated=branch_negated, |                     child, can_reuse=used_aliases, branch_negated=branch_negated, | ||||||
|                     current_negated=current_negated) |                     current_negated=current_negated) | ||||||
|             target_clause.add(child_clause, connector) |             target_clause.add(child_clause, connector) | ||||||
|             if connector == OR: |             if effective_connector == OR: | ||||||
|                 used = alias_diff(refcounts_before, self.alias_refcount) |                 used = alias_diff(refcounts_before, self.alias_refcount) | ||||||
|                 for alias in used: |                 for alias in used: | ||||||
|                     alias_usage_counts[alias] = alias_usage_counts.get(alias, 0) + 1 |                     alias_usage_counts[alias] = alias_usage_counts.get(alias, 0) + 1 | ||||||
|         if connector == OR: |         if effective_connector == OR: | ||||||
|             self.promote_disjunction(aliases_before, alias_usage_counts, |             self.promote_disjunction(aliases_before, alias_usage_counts, | ||||||
|                                      len(q_object.children)) |                                      len(q_object.children)) | ||||||
|         return target_clause |         return target_clause | ||||||
|   | |||||||
| @@ -2662,6 +2662,85 @@ class NullJoinPromotionOrTest(TestCase): | |||||||
|         self.assertEqual(str(qs.query).count('INNER JOIN'), 1) |         self.assertEqual(str(qs.query).count('INNER JOIN'), 1) | ||||||
|         self.assertEqual(list(qs), [self.a2]) |         self.assertEqual(list(qs), [self.a2]) | ||||||
|  |  | ||||||
|  |     def test_ticket_21748(self): | ||||||
|  |         i1 = Identifier.objects.create(name='i1') | ||||||
|  |         i2 = Identifier.objects.create(name='i2') | ||||||
|  |         i3 = Identifier.objects.create(name='i3') | ||||||
|  |         Program.objects.create(identifier=i1) | ||||||
|  |         Channel.objects.create(identifier=i1) | ||||||
|  |         Program.objects.create(identifier=i2) | ||||||
|  |         self.assertQuerysetEqual( | ||||||
|  |             Identifier.objects.filter(program=None, channel=None), | ||||||
|  |             [i3], lambda x: x) | ||||||
|  |         self.assertQuerysetEqual( | ||||||
|  |             Identifier.objects.exclude(program=None, channel=None).order_by('name'), | ||||||
|  |             [i1, i2], lambda x: x) | ||||||
|  |  | ||||||
|  |     def test_ticket_21748_double_negated_and(self): | ||||||
|  |         i1 = Identifier.objects.create(name='i1') | ||||||
|  |         i2 = Identifier.objects.create(name='i2') | ||||||
|  |         Identifier.objects.create(name='i3') | ||||||
|  |         p1 = Program.objects.create(identifier=i1) | ||||||
|  |         c1 = Channel.objects.create(identifier=i1) | ||||||
|  |         Program.objects.create(identifier=i2) | ||||||
|  |         # Check the ~~Q() (or equivalently .exclude(~Q)) works like Q() for | ||||||
|  |         # join promotion. | ||||||
|  |         qs1_doubleneg = Identifier.objects.exclude(~Q(program__id=p1.id, channel__id=c1.id)).order_by('pk') | ||||||
|  |         qs1_filter = Identifier.objects.filter(program__id=p1.id, channel__id=c1.id).order_by('pk') | ||||||
|  |         self.assertQuerysetEqual(qs1_doubleneg, qs1_filter, lambda x: x) | ||||||
|  |         self.assertEqual(str(qs1_filter.query).count('JOIN'), | ||||||
|  |                          str(qs1_doubleneg.query).count('JOIN')) | ||||||
|  |         self.assertEqual(2, str(qs1_doubleneg.query).count('INNER JOIN')) | ||||||
|  |         self.assertEqual(str(qs1_filter.query).count('INNER JOIN'), | ||||||
|  |                          str(qs1_doubleneg.query).count('INNER JOIN')) | ||||||
|  |  | ||||||
|  |     def test_ticket_21748_double_negated_or(self): | ||||||
|  |         i1 = Identifier.objects.create(name='i1') | ||||||
|  |         i2 = Identifier.objects.create(name='i2') | ||||||
|  |         Identifier.objects.create(name='i3') | ||||||
|  |         p1 = Program.objects.create(identifier=i1) | ||||||
|  |         c1 = Channel.objects.create(identifier=i1) | ||||||
|  |         p2 = Program.objects.create(identifier=i2) | ||||||
|  |         # Test OR + doubleneq. The expected result is that channel is LOUTER | ||||||
|  |         # joined, program INNER joined | ||||||
|  |         qs1_filter = Identifier.objects.filter( | ||||||
|  |             Q(program__id=p2.id, channel__id=c1.id) | ||||||
|  |             | Q(program__id=p1.id) | ||||||
|  |         ).order_by('pk') | ||||||
|  |         qs1_doubleneg = Identifier.objects.exclude( | ||||||
|  |             ~Q(Q(program__id=p2.id, channel__id=c1.id) | ||||||
|  |             | Q(program__id=p1.id)) | ||||||
|  |         ).order_by('pk') | ||||||
|  |         self.assertQuerysetEqual(qs1_doubleneg, qs1_filter, lambda x: x) | ||||||
|  |         self.assertEqual(str(qs1_filter.query).count('JOIN'), | ||||||
|  |                          str(qs1_doubleneg.query).count('JOIN')) | ||||||
|  |         self.assertEqual(1, str(qs1_doubleneg.query).count('INNER JOIN')) | ||||||
|  |         self.assertEqual(str(qs1_filter.query).count('INNER JOIN'), | ||||||
|  |                          str(qs1_doubleneg.query).count('INNER JOIN')) | ||||||
|  |  | ||||||
|  |     def test_ticket_21748_complex_filter(self): | ||||||
|  |         i1 = Identifier.objects.create(name='i1') | ||||||
|  |         i2 = Identifier.objects.create(name='i2') | ||||||
|  |         Identifier.objects.create(name='i3') | ||||||
|  |         p1 = Program.objects.create(identifier=i1) | ||||||
|  |         c1 = Channel.objects.create(identifier=i1) | ||||||
|  |         p2 = Program.objects.create(identifier=i2) | ||||||
|  |         # Finally, a more complex case, one time in a way where each | ||||||
|  |         # NOT is pushed to lowest level in the boolean tree, and | ||||||
|  |         # another query where this isn't done. | ||||||
|  |         qs1 = Identifier.objects.filter( | ||||||
|  |             ~Q(~Q(program__id=p2.id, channel__id=c1.id) | ||||||
|  |             & Q(program__id=p1.id))).order_by('pk') | ||||||
|  |         qs2 = Identifier.objects.filter( | ||||||
|  |             Q(Q(program__id=p2.id, channel__id=c1.id) | ||||||
|  |             | ~Q(program__id=p1.id))).order_by('pk') | ||||||
|  |         self.assertQuerysetEqual(qs1, qs2, lambda x: x) | ||||||
|  |         self.assertEqual(str(qs1.query).count('JOIN'), | ||||||
|  |                          str(qs2.query).count('JOIN')) | ||||||
|  |         self.assertEqual(0, str(qs1.query).count('INNER JOIN')) | ||||||
|  |         self.assertEqual(str(qs1.query).count('INNER JOIN'), | ||||||
|  |                          str(qs2.query).count('INNER JOIN')) | ||||||
|  |  | ||||||
|  |  | ||||||
| class ReverseJoinTrimmingTest(TestCase): | class ReverseJoinTrimmingTest(TestCase): | ||||||
|     def test_reverse_trimming(self): |     def test_reverse_trimming(self): | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user