Loading django/db/models/sql/expressions.py +2 −2 Original line number Diff line number Diff line Loading @@ -52,10 +52,10 @@ class SQLEvaluator(object): field, source, opts, join_list, path = query.setup_joins( field_list, query.get_meta(), query.get_initial_alias(), self.reuse) col, _, join_list = query.trim_joins(source, join_list, path) target, _, join_list = query.trim_joins(source, join_list, path) if self.reuse is not None: self.reuse.update(join_list) self.cols.append((node, (join_list[-1], col))) self.cols.append((node, (join_list[-1], target.column))) except FieldDoesNotExist: raise FieldError("Cannot resolve keyword %r into field. " "Choices are: %s" % (self.name, Loading django/db/models/sql/query.py +29 −53 Original line number Diff line number Diff line Loading @@ -1093,14 +1093,14 @@ class Query(object): field_list, opts, self.get_initial_alias()) # Process the join chain to see if it can be trimmed col, _, join_list = self.trim_joins(source, join_list, path) target, _, join_list = self.trim_joins(source, join_list, path) # If the aggregate references a model or field that requires a join, # those joins must be LEFT OUTER - empty join rows must be returned # in order for zeros to be returned for those aggregates. self.promote_joins(join_list, True) col = (join_list[-1], col) col = (join_list[-1], target.column) else: # The simplest cases. No joins required - # just reference the provided column alias. Loading Loading @@ -1214,46 +1214,30 @@ class Query(object): # the far end (fewer tables in a query is better). Note that join # promotion must happen before join trimming to have the join type # information available when reusing joins. col, alias, join_list = self.trim_joins(target, join_list, path) target, alias, join_list = self.trim_joins(target, join_list, path) if having_clause or force_having: if (alias, col) not in self.group_by: self.group_by.append((alias, col)) self.having.add((Constraint(alias, col, field), lookup_type, value), if (alias, target.column) not in self.group_by: self.group_by.append((alias, target.column)) self.having.add((Constraint(alias, target.column, field), lookup_type, value), connector) else: self.where.add((Constraint(alias, col, field), lookup_type, value), self.where.add((Constraint(alias, target.column, field), lookup_type, value), connector) if negate: self.promote_joins(join_list) if lookup_type != 'isnull': if len(join_list) > 1: for alias in join_list: if self.alias_map[alias].join_type == self.LOUTER: j_col = self.alias_map[alias].rhs_join_col # The join promotion logic should never produce # a LOUTER join for the base join - assert that. assert j_col is not None entry = self.where_class() entry.add( (Constraint(alias, j_col, None), 'isnull', True), AND ) entry.negate() self.where.add(entry, AND) break if self.is_nullable(field): # In SQL NULL = anyvalue returns unknown, and NOT unknown # is still unknown. However, in Python None = anyvalue is False # (and not False is True...), and we want to return this Python's # view of None handling. So we need to specifically exclude the # NULL values, and because we are inside NOT branch they will # be included in the final resultset. We are essentially creating # SQL like this here: NOT (col IS NOT NULL), where the first NOT # is added in upper layers of the code. self.where.add((Constraint(alias, col, None), 'isnull', False), AND) if lookup_type != 'isnull' and (self.is_nullable(target) or len(join_list) > 1): # The condition added here will be SQL like this: # NOT (col IS NOT NULL), where the first NOT is added in # upper layers of code. The reason for addition is that if col # is null, then col != someval will result in SQL "unknown" # which isn't the same as in Python. The Python None handling # is wanted, and it can be gotten by # (col IS NULL OR col != someval) # <=> # NOT (col IS NOT NULL AND col = someval). self.where.add((Constraint(alias, target.column, None), 'isnull', False), AND) def add_q(self, q_object, used_aliases=None, force_having=False): """ Loading Loading @@ -1437,7 +1421,7 @@ class Query(object): is the full list of join aliases. The 'path' contain the PathInfos used to create the joins. Returns the final active column and table alias and the new active Returns the final target field and table alias and the new active joins. We will always trim any direct join if we have the target column Loading @@ -1451,7 +1435,7 @@ class Query(object): self.unref_alias(joins.pop()) else: break return target.column, joins[-1], joins return target, joins[-1], joins def split_exclude(self, filter_expr, prefix, can_reuse, names_with_path): """ Loading @@ -1465,8 +1449,8 @@ class Query(object): can_reuse is a set of joins usable for filters in the original query. We will turn this into equivalent of: WHERE pk NOT IN (SELECT parent_id FROM thetable WHERE name = 'foo' AND parent_id IS NOT NULL) WHERE NOT (pk IN (SELECT parent_id FROM thetable WHERE name = 'foo' AND parent_id IS NOT NULL)) It might be worth it to consider using WHERE NOT EXISTS as that has saner null handling, and is easier for the backend's optimizer to Loading @@ -1484,6 +1468,7 @@ class Query(object): # since we are adding a IN <subquery> clause. This prevents the # database from tripping over IN (...,NULL,...) selects and returning # nothing if self.is_nullable(query.select[0].field): alias, col = query.select[0].col query.where.add((Constraint(alias, col, None), 'isnull', False), AND) Loading @@ -1506,15 +1491,6 @@ class Query(object): self.add_filter(('%s__in' % trimmed_prefix, query), negate=True, can_reuse=can_reuse) # If there's more than one join in the inner query, we need to also # handle the possibility that the earlier joins don't match anything # by adding a comparison to NULL (e.g. in # Tag.objects.exclude(parent__parent__name='t1') # a tag with no parent would otherwise be overlooked). if trimmed_joins > 1: self.add_filter(('%s__isnull' % trimmed_prefix, False), negate=True, can_reuse=can_reuse) def set_empty(self): self.where = EmptyWhere() self.having = EmptyWhere() Loading Loading @@ -1889,13 +1865,13 @@ class Query(object): if self.alias_map[peek].join_type == self.LOUTER: # Back up one level and break select_alias = self.tables[join_pos] select_col = path.from_field.column select_field = path.from_field break select_alias = self.tables[join_pos + 1] select_col = path.to_field.column select_field = path.to_field self.unref_alias(self.tables[join_pos]) join_pos += 1 self.select = [SelectInfo((select_alias, select_col), None)] self.select = [SelectInfo((select_alias, select_field.column), select_field)] self.remove_inherited_models() return join_pos Loading tests/regressiontests/nested_foreign_keys/tests.py +6 −0 Original line number Diff line number Diff line Loading @@ -71,6 +71,12 @@ class NestedForeignKeysTests(TestCase): self.assertEqual(Event.objects.filter(screeningnullfk__movie=self.movie).count(), 1) self.assertEqual(Event.objects.exclude(screeningnullfk__movie=self.movie).count(), 2) def test_null_exclude(self): screening = ScreeningNullFK.objects.create(movie=None) ScreeningNullFK.objects.create(movie=self.movie) self.assertEqual( list(ScreeningNullFK.objects.exclude(movie__id=self.movie.pk)), [screening]) # This test failed in #16715 because in some cases INNER JOIN was selected # for the second foreign key relation instead of LEFT OUTER JOIN. Loading Loading
django/db/models/sql/expressions.py +2 −2 Original line number Diff line number Diff line Loading @@ -52,10 +52,10 @@ class SQLEvaluator(object): field, source, opts, join_list, path = query.setup_joins( field_list, query.get_meta(), query.get_initial_alias(), self.reuse) col, _, join_list = query.trim_joins(source, join_list, path) target, _, join_list = query.trim_joins(source, join_list, path) if self.reuse is not None: self.reuse.update(join_list) self.cols.append((node, (join_list[-1], col))) self.cols.append((node, (join_list[-1], target.column))) except FieldDoesNotExist: raise FieldError("Cannot resolve keyword %r into field. " "Choices are: %s" % (self.name, Loading
django/db/models/sql/query.py +29 −53 Original line number Diff line number Diff line Loading @@ -1093,14 +1093,14 @@ class Query(object): field_list, opts, self.get_initial_alias()) # Process the join chain to see if it can be trimmed col, _, join_list = self.trim_joins(source, join_list, path) target, _, join_list = self.trim_joins(source, join_list, path) # If the aggregate references a model or field that requires a join, # those joins must be LEFT OUTER - empty join rows must be returned # in order for zeros to be returned for those aggregates. self.promote_joins(join_list, True) col = (join_list[-1], col) col = (join_list[-1], target.column) else: # The simplest cases. No joins required - # just reference the provided column alias. Loading Loading @@ -1214,46 +1214,30 @@ class Query(object): # the far end (fewer tables in a query is better). Note that join # promotion must happen before join trimming to have the join type # information available when reusing joins. col, alias, join_list = self.trim_joins(target, join_list, path) target, alias, join_list = self.trim_joins(target, join_list, path) if having_clause or force_having: if (alias, col) not in self.group_by: self.group_by.append((alias, col)) self.having.add((Constraint(alias, col, field), lookup_type, value), if (alias, target.column) not in self.group_by: self.group_by.append((alias, target.column)) self.having.add((Constraint(alias, target.column, field), lookup_type, value), connector) else: self.where.add((Constraint(alias, col, field), lookup_type, value), self.where.add((Constraint(alias, target.column, field), lookup_type, value), connector) if negate: self.promote_joins(join_list) if lookup_type != 'isnull': if len(join_list) > 1: for alias in join_list: if self.alias_map[alias].join_type == self.LOUTER: j_col = self.alias_map[alias].rhs_join_col # The join promotion logic should never produce # a LOUTER join for the base join - assert that. assert j_col is not None entry = self.where_class() entry.add( (Constraint(alias, j_col, None), 'isnull', True), AND ) entry.negate() self.where.add(entry, AND) break if self.is_nullable(field): # In SQL NULL = anyvalue returns unknown, and NOT unknown # is still unknown. However, in Python None = anyvalue is False # (and not False is True...), and we want to return this Python's # view of None handling. So we need to specifically exclude the # NULL values, and because we are inside NOT branch they will # be included in the final resultset. We are essentially creating # SQL like this here: NOT (col IS NOT NULL), where the first NOT # is added in upper layers of the code. self.where.add((Constraint(alias, col, None), 'isnull', False), AND) if lookup_type != 'isnull' and (self.is_nullable(target) or len(join_list) > 1): # The condition added here will be SQL like this: # NOT (col IS NOT NULL), where the first NOT is added in # upper layers of code. The reason for addition is that if col # is null, then col != someval will result in SQL "unknown" # which isn't the same as in Python. The Python None handling # is wanted, and it can be gotten by # (col IS NULL OR col != someval) # <=> # NOT (col IS NOT NULL AND col = someval). self.where.add((Constraint(alias, target.column, None), 'isnull', False), AND) def add_q(self, q_object, used_aliases=None, force_having=False): """ Loading Loading @@ -1437,7 +1421,7 @@ class Query(object): is the full list of join aliases. The 'path' contain the PathInfos used to create the joins. Returns the final active column and table alias and the new active Returns the final target field and table alias and the new active joins. We will always trim any direct join if we have the target column Loading @@ -1451,7 +1435,7 @@ class Query(object): self.unref_alias(joins.pop()) else: break return target.column, joins[-1], joins return target, joins[-1], joins def split_exclude(self, filter_expr, prefix, can_reuse, names_with_path): """ Loading @@ -1465,8 +1449,8 @@ class Query(object): can_reuse is a set of joins usable for filters in the original query. We will turn this into equivalent of: WHERE pk NOT IN (SELECT parent_id FROM thetable WHERE name = 'foo' AND parent_id IS NOT NULL) WHERE NOT (pk IN (SELECT parent_id FROM thetable WHERE name = 'foo' AND parent_id IS NOT NULL)) It might be worth it to consider using WHERE NOT EXISTS as that has saner null handling, and is easier for the backend's optimizer to Loading @@ -1484,6 +1468,7 @@ class Query(object): # since we are adding a IN <subquery> clause. This prevents the # database from tripping over IN (...,NULL,...) selects and returning # nothing if self.is_nullable(query.select[0].field): alias, col = query.select[0].col query.where.add((Constraint(alias, col, None), 'isnull', False), AND) Loading @@ -1506,15 +1491,6 @@ class Query(object): self.add_filter(('%s__in' % trimmed_prefix, query), negate=True, can_reuse=can_reuse) # If there's more than one join in the inner query, we need to also # handle the possibility that the earlier joins don't match anything # by adding a comparison to NULL (e.g. in # Tag.objects.exclude(parent__parent__name='t1') # a tag with no parent would otherwise be overlooked). if trimmed_joins > 1: self.add_filter(('%s__isnull' % trimmed_prefix, False), negate=True, can_reuse=can_reuse) def set_empty(self): self.where = EmptyWhere() self.having = EmptyWhere() Loading Loading @@ -1889,13 +1865,13 @@ class Query(object): if self.alias_map[peek].join_type == self.LOUTER: # Back up one level and break select_alias = self.tables[join_pos] select_col = path.from_field.column select_field = path.from_field break select_alias = self.tables[join_pos + 1] select_col = path.to_field.column select_field = path.to_field self.unref_alias(self.tables[join_pos]) join_pos += 1 self.select = [SelectInfo((select_alias, select_col), None)] self.select = [SelectInfo((select_alias, select_field.column), select_field)] self.remove_inherited_models() return join_pos Loading
tests/regressiontests/nested_foreign_keys/tests.py +6 −0 Original line number Diff line number Diff line Loading @@ -71,6 +71,12 @@ class NestedForeignKeysTests(TestCase): self.assertEqual(Event.objects.filter(screeningnullfk__movie=self.movie).count(), 1) self.assertEqual(Event.objects.exclude(screeningnullfk__movie=self.movie).count(), 2) def test_null_exclude(self): screening = ScreeningNullFK.objects.create(movie=None) ScreeningNullFK.objects.create(movie=self.movie) self.assertEqual( list(ScreeningNullFK.objects.exclude(movie__id=self.movie.pk)), [screening]) # This test failed in #16715 because in some cases INNER JOIN was selected # for the second foreign key relation instead of LEFT OUTER JOIN. Loading