Ticket #10790: ticket10790_v5.diff

File ticket10790_v5.diff, 13.0 KB (added by milosu, 3 years ago)
  • django/db/models/sql/compiler.py

    diff -urN Django-1.4b1.orig/django/db/models/sql/compiler.py Django-1.4b1/django/db/models/sql/compiler.py
    old new  
    456456        """
    457457        if not alias:
    458458            alias = self.query.get_initial_alias()
    459         field, target, opts, joins, _, _ = self.query.setup_joins(pieces,
    460                 opts, alias, False)
     459        field, target, opts, joins, _, _, _ = self.query.setup_joins(
     460                pieces, opts, alias, False)
    461461        alias = joins[-1]
    462462        col = target.column
    463463        if not field.rel:
     
    511511            if not self.query.alias_refcount[alias]:
    512512                continue
    513513            try:
    514                 name, alias, join_type, lhs, lhs_col, col, nullable = self.query.alias_map[alias]
     514                name, alias, join_type, lhs, lhs_col, col, nullable, demoted_join = self.query.alias_map[alias]
    515515            except KeyError:
    516516                # Extra tables can end up in self.tables, but not in the
    517517                # alias_map if they aren't in a join. That's OK. We skip them.
  • django/db/models/sql/constants.py

    diff -urN Django-1.4b1.orig/django/db/models/sql/constants.py Django-1.4b1/django/db/models/sql/constants.py
    old new  
    2424LHS_JOIN_COL = 4
    2525RHS_JOIN_COL = 5
    2626NULLABLE = 6
     27DEMOTED_JOIN = 7
    2728
    2829# How many results to expect from a cursor.execute call
    2930MULTI = 'multi'
  • django/db/models/sql/expressions.py

    diff -urN Django-1.4b1.orig/django/db/models/sql/expressions.py Django-1.4b1/django/db/models/sql/expressions.py
    old new  
    4444            self.cols[node] = query.aggregate_select[node.name]
    4545        else:
    4646            try:
    47                 field, source, opts, join_list, last, _ = query.setup_joins(
     47                field, source, opts, join_list, last, _, _ = query.setup_joins(
    4848                    field_list, query.get_meta(),
    4949                    query.get_initial_alias(), False)
    5050                col, _, join_list = query.trim_joins(source, join_list, last, False)
  • django/db/models/sql/query.py

    diff -urN Django-1.4b1.orig/django/db/models/sql/query.py Django-1.4b1/django/db/models/sql/query.py
    old new  
    465465        conjunction = (connector == AND)
    466466        first = True
    467467        for alias in rhs.tables:
    468             if not rhs.alias_refcount[alias]:
     468            demoted_alias = rhs.alias_map[alias][DEMOTED_JOIN]
     469            # the alias can be ignored only if it was not demoted due to the fkey trim join
     470            if not rhs.alias_refcount[alias] and not demoted_alias:
    469471                # An unused alias.
    470472                continue
    471             promote = (rhs.alias_map[alias][JOIN_TYPE] == self.LOUTER)
     473            promote = (rhs.alias_map[alias][JOIN_TYPE] == self.LOUTER or demoted_alias)
    472474            lhs, table, lhs_col, col = rhs.rev_join_map[alias]
    473475            # If the left side of the join was already relabeled, use the
    474476            # updated alias.
     
    700702            return True
    701703        return False
    702704
     705    def demote_alias(self, alias):
     706        """
     707        Demotes the join type of an alias to an inner join.
     708        """
     709        data = list(self.alias_map[alias])
     710        data[JOIN_TYPE] = self.INNER
     711        data[DEMOTED_JOIN] = True
     712        self.alias_map[alias] = tuple(data)
     713
    703714    def promote_alias_chain(self, chain, must_promote=False):
    704715        """
    705716        Walks along a chain of aliases, promoting the first nullable join and
     
    906917                    if self.alias_map[alias][LHS_ALIAS] != lhs:
    907918                        continue
    908919                    self.ref_alias(alias)
    909                     if promote:
     920                    if promote or self.alias_map[alias][DEMOTED_JOIN]:
    910921                        self.promote_alias(alias)
    911922                    return alias
    912923
    913924        # No reuse is possible, so we need a new alias.
    914925        alias, _ = self.table_alias(table, True)
     926
    915927        if not lhs:
    916928            # Not all tables need to be joined to anything. No join type
    917929            # means the later columns are ignored.
    918930            join_type = None
    919931        elif promote or outer_if_first:
    920932            join_type = self.LOUTER
     933        elif lhs in self.alias_map and self.alias_map[lhs][DEMOTED_JOIN]:
     934            # if the lhs is already present in the alias_map
     935            # and its join was DEMOTED earlier, change its join type to LOUTER
     936            # and promote LOUTER join
     937            if self.alias_map[lhs][JOIN_TYPE] == self.INNER:
     938                self.promote_alias(lhs, unconditional=True)
     939            join_type = self.LOUTER
    921940        else:
    922941            join_type = self.INNER
    923         join = (table, alias, join_type, lhs, lhs_col, col, nullable)
     942        join = (table, alias, join_type, lhs, lhs_col, col, nullable, False)
    924943        self.alias_map[alias] = join
    925944        if t_ident in self.join_map:
    926945            self.join_map[t_ident] += (alias,)
     
    10071026            #   - this is an annotation over a model field
    10081027            # then we need to explore the joins that are required.
    10091028
    1010             field, source, opts, join_list, last, _ = self.setup_joins(
     1029            field, source, opts, join_list, last, _, allow_trim_join = self.setup_joins(
    10111030                field_list, opts, self.get_initial_alias(), False)
    10121031
    10131032            # Process the join chain to see if it can be trimmed
     
    11191138        allow_many = trim or not negate
    11201139
    11211140        try:
    1122             field, target, opts, join_list, last, extra_filters = self.setup_joins(
     1141            field, target, opts, join_list, last, extra_filters, allow_trim_join = self.setup_joins(
    11231142                    parts, opts, alias, True, allow_many, allow_explicit_fk=True,
    11241143                    can_reuse=can_reuse, negate=negate,
    11251144                    process_extras=process_extras)
     
    11391158            self.promote_alias_chain(join_list)
    11401159            join_promote = True
    11411160
     1161            # If we have a one2one or many2one field, we can trim the left outer
     1162            # join from the end of a list of joins.
     1163            # In order to do this, we convert alias join type back to INNER and
     1164            # trim_joins later will do the strip for us.
     1165            if allow_trim_join and field.rel:
     1166                self.demote_alias(join_list[-1])
     1167
    11421168        # Process the join list to see if we can remove any inner joins from
    11431169        # the far end (fewer tables in a query is better).
    11441170        nonnull_comparison = (lookup_type == 'isnull' and value is False)
     
    12951321        dupe_set = set()
    12961322        exclusions = set()
    12971323        extra_filters = []
     1324        allow_trim_join = True
    12981325        int_alias = None
    12991326        for pos, name in enumerate(names):
    13001327            if int_alias is not None:
     
    13181345                    raise FieldError("Cannot resolve keyword %r into field. "
    13191346                            "Choices are: %s" % (name, ", ".join(names)))
    13201347
     1348            # presence of indirect field in the filter requires
     1349            # left outer join for isnull
     1350            if not direct and allow_trim_join:
     1351                allow_trim_join = False
     1352
    13211353            if not allow_many and (m2m or not direct):
    13221354                for alias in joins:
    13231355                    self.unref_alias(alias)
     
    13591391                extra_filters.extend(field.extra_filters(names, pos, negate))
    13601392            if direct:
    13611393                if m2m:
     1394                    # null query on m2mfield requires outer join
     1395                    allow_trim_join = False
    13621396                    # Many-to-many field defined on the current model.
    13631397                    if cached_data:
    13641398                        (table1, from_col1, to_col1, table2, from_col2,
     
    14791513            else:
    14801514                raise FieldError("Join on field %r not permitted." % name)
    14811515
    1482         return field, target, opts, joins, last, extra_filters
     1516        return field, target, opts, joins, last, extra_filters, allow_trim_join
    14831517
    14841518    def trim_joins(self, target, join_list, last, trim, nonnull_check=False):
    14851519        """
     
    16481682
    16491683        try:
    16501684            for name in field_names:
    1651                 field, target, u2, joins, u3, u4 = self.setup_joins(
     1685                field, target, u2, joins, u3, u4, allow_trim_join = self.setup_joins(
    16521686                        name.split(LOOKUP_SEP), opts, alias, False, allow_m2m,
    16531687                        True)
    16541688                final_alias = joins[-1]
     
    19301964        """
    19311965        opts = self.model._meta
    19321966        alias = self.get_initial_alias()
    1933         field, col, opts, joins, last, extra = self.setup_joins(
     1967        field, col, opts, joins, last, extra, allow_trim_join = self.setup_joins(
    19341968                start.split(LOOKUP_SEP), opts, alias, False)
    19351969        select_col = self.alias_map[joins[1]][LHS_JOIN_COL]
    19361970        select_alias = alias
  • tests/modeltests/null_trimjoin/__init__.py

    diff -urN Django-1.4b1.orig/tests/modeltests/null_trimjoin/__init__.py Django-1.4b1/tests/modeltests/null_trimjoin/__init__.py
    old new  
     1# dummy text for patch
  • tests/modeltests/null_trimjoin/models.py

    diff -urN Django-1.4b1.orig/tests/modeltests/null_trimjoin/models.py Django-1.4b1/tests/modeltests/null_trimjoin/models.py
    old new  
     1"""
     2Do not join table when querying on isnull
     3
     4"""
     5
     6from django.db import models
     7
     8class Category(models.Model):
     9    name = models.CharField(max_length=30)
     10
     11class ReporterType(models.Model):
     12    name = models.CharField(max_length=30)
     13
     14class Reporter(models.Model):
     15    name = models.CharField(max_length=30)
     16    type = models.ForeignKey(ReporterType, null=True)
     17    category = models.ManyToManyField(Category, null=True)
     18
     19    def __unicode__(self):
     20        return self.name
     21
     22class Article(models.Model):
     23    headline = models.CharField(max_length=100)
     24    reporter = models.ForeignKey(Reporter, null=True)
     25
     26    class Meta:
     27        ordering = ('headline',)
     28
     29    def __unicode__(self):
     30        return self.headline
     31
  • tests/modeltests/null_trimjoin/tests.py

    diff -urN Django-1.4b1.orig/tests/modeltests/null_trimjoin/tests.py Django-1.4b1/tests/modeltests/null_trimjoin/tests.py
    old new  
     1from django.test import TestCase
     2from models import Article, Reporter
     3
     4class OneToOneTests(TestCase):
     5
     6    def setUp(self):
     7        self.r = Reporter(name='John Smith')
     8        self.r.save()
     9        self.a = Article(headline="First", reporter=self.r)
     10        self.a.save()
     11        self.a2 = Article(headline="Second")
     12        self.a2.save()
     13
     14    def test_query_with_isnull(self):
     15        """Querying with isnull should not join Reporter table."""
     16        q = Article.objects.filter(reporter=None)
     17        # check that reporter is not in the query's used_aliases
     18        self.assertFalse('null_trimjoin_reporter' in q.query.used_aliases)
     19        self.assertTrue('null_trimjoin_article' in q.query.used_aliases)
     20        # but it should still be in query.tables
     21        self.assertTrue('null_trimjoin_article' in q.query.tables)
     22        self.assertTrue('null_trimjoin_reporter' in q.query.tables)
     23
     24    def test_query_across_tables(self):
     25        """Querying across several tables should strip only the last join, while
     26        preserving the preceding left outer joins."""
     27        q = Article.objects.filter(reporter__type=None)
     28        self.assertEquals(len(q), 2)
     29        self.assertTrue('null_trimjoin_article' in q.query.used_aliases)
     30        self.assertTrue('null_trimjoin_reporter' in q.query.used_aliases)
     31        self.assertFalse('null_trimjoin_reportertype' in q.query.used_aliases)
     32
     33    def test_m2m_query(self):
     34        """Querying across m2m field should not strip the m2m table from join."""
     35        q = Article.objects.filter(reporter__category__isnull=True)
     36        self.assertTrue('null_trimjoin_article' in q.query.used_aliases)
     37        self.assertTrue('null_trimjoin_reporter' in q.query.used_aliases)
     38        self.assertTrue('null_trimjoin_category' in q.query.used_aliases)
     39
     40    def test_reverse_query(self):
     41        """Reverse querying with isnull should not strip the join."""
     42        q = Reporter.objects.filter(article__isnull=True)
     43        self.assertTrue('null_trimjoin_reporter' in q.query.used_aliases)
     44
     45
Back to Top