Django

Code

Changeset 7170

Show
Ignore:
Timestamp:
02/28/08 06:57:10 (9 months ago)
Author:
mtredinnick
Message:

queryset-refactor: Fixed exclude() filtering for the various N-to-many relations.

This means we can now do nested SQL queries (since we need nested queries to
get the right answer). It requires poking directly at the Query class. Might
add support for this through QuerySets? later.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • django/branches/queryset-refactor/django/db/models/fields/related.py

    r7142 r7170  
    101101            return v 
    102102 
     103        if hasattr(value, 'as_sql'): 
     104            sql, params = value.as_sql() 
     105            return ('(%s)' % sql), params 
    103106        if lookup_type == 'exact': 
    104107            return [pk_trace(value)] 
  • django/branches/queryset-refactor/django/db/models/sql/query.py

    r7165 r7170  
    4646        self.default_ordering = True 
    4747        self.standard_ordering = True 
     48        self.start_meta = None 
    4849 
    4950        # SQL-related attributes 
     
    8182        sql, params = self.as_sql() 
    8283        return sql % params 
     84 
     85    def __deepcopy__(self, memo): 
     86        result= self.clone() 
     87        memo[id(self)] = result 
     88        return result 
     89 
     90    def get_meta(self): 
     91        """ 
     92        Returns the Options instance (the model._meta) from which to start 
     93        processing. Normally, this is self.model._meta, but it can change. 
     94        """ 
     95        if self.start_meta: 
     96            return self.start_meta 
     97        return self.model._meta 
    8398 
    8499    def quote_name_unless_alias(self, name): 
     
    115130        obj.default_ordering = self.default_ordering 
    116131        obj.standard_ordering = self.standard_ordering 
     132        obj.start_meta = self.start_meta 
    117133        obj.select = self.select[:] 
    118134        obj.tables = self.tables[:] 
     
    385401                alias_str = '' 
    386402                name = alias 
    387             if join_type
     403            if join_type and not first
    388404                result.append('%s %s%s ON (%s.%s = %s.%s)' 
    389405                        % (join_type, qn(name), alias_str, qn(lhs), 
     
    650666            return 
    651667        if not opts: 
    652             opts = self.model._meta 
     668            opts = self.get_meta() 
    653669            root_alias = self.tables[0] 
    654670            self.select.extend([(root_alias, f.column) for f in opts.fields]) 
     
    682698                    used, next, restricted) 
    683699 
    684     def add_filter(self, filter_expr, connector=AND, negate=False): 
    685         """ 
    686         Add a single filter to the query. 
     700    def add_filter(self, filter_expr, connector=AND, negate=False, trim=False): 
     701        """ 
     702        Add a single filter to the query. The 'filter_expr' is a pair: 
     703        (filter_string, value). E.g. ('name__contains', 'fred') 
     704 
     705        If 'negate' is True, this is an exclude() filter. If 'trim' is True, we 
     706        automatically trim the final join group (used internally when 
     707        constructing nested queries). 
    687708        """ 
    688709        arg, value = filter_expr 
     
    707728            value = value() 
    708729 
    709         opts = self.model._meta 
     730        opts = self.get_meta() 
    710731        alias = self.join((None, opts.db_table, None, None)) 
    711  
    712         field, target, opts, join_list, = self.setup_joins(parts, opts, 
    713                 alias, (connector == AND)) 
    714         col = target.column 
     732        allow_many = trim or not negate 
     733 
     734        result = self.setup_joins(parts, opts, alias, (connector == AND), 
     735                allow_many) 
     736        if isinstance(result, int): 
     737            self.split_exclude(filter_expr, LOOKUP_SEP.join(parts[:result])) 
     738            return 
     739        field, target, opts, join_list = result 
     740        if trim and len(join_list) > 1: 
     741            extra = join_list[-1] 
     742            join_list = join_list[:-1] 
     743            col = self.alias_map[extra[0]][ALIAS_JOIN][LHS_JOIN_COL] 
     744            for alias in extra: 
     745                self.unref_alias(alias) 
     746        else: 
     747            col = target.column 
    715748        alias = join_list[-1][-1] 
    716749 
     
    730763            # If the comparison is against NULL, we need to use a left outer 
    731764            # join when connecting to the previous model. We make that 
    732             # adjustment here. We don't do this unless needed because it's less 
     765            # adjustment here. We don't do this unless needed as it's less 
    733766            # efficient at the database level. 
    734767            self.promote_alias(join_list[-1][0]) 
     
    768801            self.where.negate() 
    769802            if flag: 
     803                # XXX: Change this to the field we joined against to allow 
     804                # for node sharing and where-tree optimisation? 
    770805                self.where.add([alias, col, field, 'isnull', True], OR) 
    771806 
     
    798833            self.where.end_subtree() 
    799834 
    800     def setup_joins(self, names, opts, alias, dupe_multis): 
     835    def setup_joins(self, names, opts, alias, dupe_multis, allow_many=True): 
    801836        """ 
    802837        Compute the necessary table joins for the passage through the fields 
     
    808843 
    809844        Returns the final field involved in the join, the target database 
    810         column (used for any 'where' constraint), the final 'opts' value, the 
    811         list of tables joined and a list indicating whether or not each join 
    812         can be null. 
     845        column (used for any 'where' constraint), the final 'opts' value and the 
     846        list of tables joined. 
    813847        """ 
    814848        joins = [[alias]] 
     
    823857                raise FieldError("Cannot resolve keyword %r into field. " 
    824858                        "Choices are: %s" % (name, ", ".join(names))) 
     859            if not allow_many and (m2m or not direct): 
     860                for join in joins: 
     861                    for alias in join: 
     862                        self.unref_alias(alias) 
     863                return pos + 1 
    825864            if model: 
    826865                # The field lives on a base class of the current model. 
     
    930969        return field, target, opts, joins 
    931970 
     971    def split_exclude(self, filter_expr, prefix): 
     972        """ 
     973        When doing an exclude against any kind of N-to-many relation, we need 
     974        to use a subquery. This method constructs the nested query, given the 
     975        original exclude filter (filter_expr) and the portion up to the first 
     976        N-to-many relation field. 
     977        """ 
     978        query = Query(self.model, self.connection) 
     979        query.add_filter(filter_expr) 
     980        query.set_start(prefix) 
     981        query.clear_ordering(True) 
     982        self.add_filter(('%s__in' % prefix, query), negate=True, trim=True) 
     983 
    932984    def set_limits(self, low=None, high=None): 
    933985        """ 
     
    10481100        self.select_related = field_dict 
    10491101 
     1102    def set_start(self, start): 
     1103        """ 
     1104        Sets the table from which to start joining. The start position is 
     1105        specified by the related attribute from the base model. This will 
     1106        automatically set to the select column to be the column linked from the 
     1107        previous table. 
     1108 
     1109        This method is primarily for internal use and the error checking isn't 
     1110        as friendly as add_filter(). Mostly useful for querying directly 
     1111        against the join table of many-to-many relation in a subquery. 
     1112        """ 
     1113        opts = self.model._meta 
     1114        alias = self.join((None, opts.db_table, None, None)) 
     1115        field, col, opts, joins = self.setup_joins(start.split(LOOKUP_SEP), 
     1116                opts, alias, False) 
     1117        alias = joins[-1][0] 
     1118        self.select = [(alias, self.alias_map[alias][ALIAS_JOIN][RHS_JOIN_COL])] 
     1119        self.start_meta = opts 
     1120 
     1121        # The call to setup_joins add an extra reference to everything in 
     1122        # joins. So we need to unref everything once, and everything prior to 
     1123        # the final join a second time. 
     1124        for join in joins[:-1]: 
     1125            for alias in join: 
     1126                self.unref_alias(alias) 
     1127                self.unref_alias(alias) 
     1128        for alias in joins[-1]: 
     1129            self.unref_alias(alias) 
     1130 
    10501131    def execute_sql(self, result_type=MULTI): 
    10511132        """ 
  • django/branches/queryset-refactor/django/db/models/sql/where.py

    r7169 r7170  
    106106            cast_sql = '%s' 
    107107 
    108         format = "%s %%s" % connection.ops.lookup_cast(lookup_type) 
    109108        params = field.get_db_prep_lookup(lookup_type, value) 
     109        if isinstance(params, tuple): 
     110            extra, params = params 
     111        else: 
     112            extra = '' 
    110113 
    111114        if lookup_type in connection.operators: 
     115            format = "%s %%s %s" % (connection.ops.lookup_cast(lookup_type), 
     116                    extra) 
    112117            return (format % (field_sql, 
    113118                    connection.operators[lookup_type] % cast_sql), params) 
     
    116121            if not value: 
    117122                raise EmptyResultSet 
     123            if extra: 
     124                return ('%s IN %s' % (field_sql, extra), params) 
    118125            return ('%s IN (%s)' % (field_sql, ', '.join(['%s'] * len(value))), 
    119126                    params) 
    120127        elif lookup_type in ('range', 'year'): 
    121             return ('%s BETWEEN %%s and %%s' % field_sql, 
    122                     params) 
     128            return ('%s BETWEEN %%s and %%s' % field_sql, params) 
    123129        elif lookup_type in ('month', 'day'): 
    124130            return ('%s = %%s' % connection.ops.date_extract_sql(lookup_type, 
  • django/branches/queryset-refactor/tests/modeltests/many_to_many/models.py

    r5876 r7170  
    127127[<Publication: Highlights for Children>, <Publication: Science News>, <Publication: Science Weekly>, <Publication: The Python Journal>] 
    128128 
     129# Excluding a related item works as you would expect, too (although the SQL 
     130# involved is a little complex). 
     131>>> Article.objects.exclude(publications=p2) 
     132[<Article: Django lets you build Web apps easily>] 
     133 
    129134# If we delete a Publication, its Articles won't be able to access it. 
    130135>>> p1.delete() 
  • django/branches/queryset-refactor/tests/regressiontests/queries/models.py

    r7163 r7170  
    323323[<Author: a2>, <Author: a3>, <Author: a4>] 
    324324 
     325 
     326# Excluding across a m2m relation when there is more than one related object 
     327# associated was problematic. 
     328>>> Item.objects.exclude(tags__name='t1').order_by('name') 
     329[<Item: four>, <Item: three>] 
     330>>> Item.objects.exclude(tags__name='t1').exclude(tags__name='t4') 
     331[<Item: three>] 
     332 
    325333# Excluding from a relation that cannot be NULL should not use outer joins. 
    326334>>> query = Item.objects.exclude(creator__in=[a1, a2]).query 
    327335>>> query.LOUTER not in [x[2][2] for x in query.alias_map.values()] 
    328336True 
    329  
    330 # When only one of the joins is nullable (here, the Author -> Item join), we 
    331 # should only get outer joins after that point (one, in this case). We also 
    332 # show that three tables (so, two joins) are involved. 
    333 >>> qs = Report.objects.exclude(creator__item__name='one') 
    334 >>> list(qs) 
    335 [<Report: r2>] 
    336 >>> len([x[2][2] for x in qs.query.alias_map.values() if x[2][2] == query.LOUTER]) 
    337 1 
    338 >>> len(qs.query.alias_map) 
    339 3 
    340337 
    341338Similarly, when one of the joins cannot possibly, ever, involve NULL values (Author -> ExtraInfo, in the following), it should never be promoted to a left outer join. So hte following query should only involve one "left outer" join (Author -> Item is 0-to-many).