Ticket #3691: ticket_3691_axiak.diff

File ticket_3691_axiak.diff, 7.2 KB (added by axiak@…, 17 years ago)

Patch to django for QSplit

  • db/models/query.py

     
    636636
    637637class QOperator(object):
    638638    "Base class for QAnd and QOr"
     639    underneath_an_or = False # used to track ORs
     640    checked_or       = False
     641   
     642    def search_for_or(self):
     643        " Returns true if an or exists beneath this Q operator. "
     644        if type(self) == QOr:
     645            return True
     646
     647        for val in self.args:
     648            if val.search_for_or():
     649                return True
     650
     651        return False # there was no OR below this.
     652
     653    def set_or_found(self, or_value):
     654        if self.checked_or:
     655            return
     656
     657        self.underneath_an_or = or_value
     658        self.checked_or       = True
     659
     660        for val in self.args:
     661            val.set_or_found(or_value) # set for all the children
     662       
    639663    def __init__(self, *args):
    640664        self.args = args
    641665
    642666    def get_sql(self, opts):
     667
     668        if not self.checked_or:
     669            found_an_or = self.search_for_or()
     670            self.set_or_found(found_an_or)
     671
     672        # now that we've finished building the query
     673        self.checked_or = False
     674
    643675        joins, where, params = SortedDict(), [], []
    644676        for val in self.args:
    645677            try:
     
    684716
    685717class Q(object):
    686718    "Encapsulates queries as objects that can be combined logically."
     719    underneath_an_or = False
     720    checked_or       = False
     721    outer_join       = 'LEFT OUTER JOIN'
     722    inner_join       = 'INNER JOIN'
     723   
    687724    def __init__(self, **kwargs):
    688725        self.kwargs = kwargs
    689726
     
    693730    def __or__(self, other):
    694731        return QOr(self, other)
    695732
     733    def search_for_or(self):
     734        ' Returns false, since there are no OR Qs below. '
     735        return False
     736
     737    def set_or_found(self, or_found):
     738        """ Set whether or not we have an OR above us. """
     739        if not self.checked_or:
     740            self.underneath_an_or = or_found
     741            self.checked_or       = True  # we checked for an OR
     742
    696743    def get_sql(self, opts):
    697         return parse_lookup(self.kwargs.items(), opts)
     744        # we will check to see if it's an OR, and change the join
     745        # based upon that
    698746
     747        if self.underneath_an_or:
     748            join_text = self.outer_join
     749        else:
     750            join_text = self.inner_join
     751
     752        joins, where, params = parse_lookup(self.kwargs.items(), opts)
     753
     754        joins2 = {}
     755        for item, key in joins.items():
     756            # now we will fix the joins dictionary with
     757            # the new type of join
     758            joins2[item] = (key[0], join_text, key[2])
     759
     760        # now that we've done creating the query,
     761        # it's best that we let ourselves search again
     762        self.checked_or = False
     763
     764        return joins2, where, params
     765
     766class QSplit(Q):
     767    """
     768    Encapsulates a single JOIN-type query into one object.
     769    This allows you to run an AND on a m2m query:
     770      If Article has a m2m relation with Tag and you run:
     771        QSplit(Q(tag__value = 'A')) & QSplit(Q(tag__value = 'B'))
     772        The above will filter for all articles with a tag with value
     773        'A' *and* a tag with a value 'B'.
     774    """
     775
     776    def __init__(self, q):
     777        " Creates a separate un-joinable Q from the Q passed in. "
     778        self.q = q
     779
     780    def search_for_or(self):
     781        ' Search for an OR beneath this QNot. '
     782        return self.q.search_for_or()
     783
     784    def set_or_found(self, or_found):
     785        """ Set whether or not we have an OR above us. """
     786        if not self.checked_or:
     787            self.underneath_an_or = or_found
     788            self.checked_or       = True  # we checked for an OR
     789           
     790            self.q.set_or_found(or_found) # now we have to make the change to our child
     791   
     792    def get_sql(self, opts):
     793        from django.conf import settings
     794        # the quote below depends on the database engine
     795        quote = settings.DATABASE_ENGINE == 'mysql' and "`" or '"'
     796
     797        joins, where, params = self.q.get_sql(opts)
     798       
     799        key_replace = {}
     800        joins2      = SortedDict()
     801        where2      = []
     802
     803        UNIQUE_HASH = hash(self) # this is the unique key
     804
     805        for key, val in joins.items():
     806            cur_key = key.strip(quote) # set the current key
     807            cur_val = '%s__%s' % (cur_key, UNIQUE_HASH)
     808
     809            key_replace[cur_key] = cur_val # add to the list of thigs to replace
     810
     811            joins2['%s%s%s' % (quote, cur_val, quote)] = val
     812
     813        # we need 2 passes since now we need to go back and replace any
     814        # leftover table names
     815        for key, val in joins2.items():
     816            joins2[key] = (val[0],val[1],replace_by_dict(val[2], key_replace))
     817
     818        where2 = [replace_by_dict(clause, key_replace) for clause in where]
     819
     820        return joins2, where2, params
     821       
     822       
    699823class QNot(Q):
    700824    "Encapsulates NOT (...) queries as objects"
    701825    def __init__(self, q):
    702826        "Creates a negation of the q object passed in."
    703827        self.q = q
    704828
     829    def search_for_or(self):
     830        ' Search for an OR beneath this QNot. '
     831        return self.q.search_for_or()
     832
     833    def set_or_found(self, or_found):
     834        """ Set whether or not we have an OR above us. """
     835        if not self.checked_or:
     836            self.underneath_an_or = or_found
     837            self.checked_or       = True  # we checked for an OR
     838           
     839            self.q.set_or_found(or_found) # now we have to make the change to our child
     840
    705841    def get_sql(self, opts):
    706842        try:
    707843            joins, where, params = self.q.get_sql(opts)
     
    776912                (qn(old_prefix), qn(f.column), qn(db_table), qn(f.rel.get_related_field().column)))
    777913            select.extend(['%s.%s' % (qn(db_table), qn(f2.column)) for f2 in f.rel.to._meta.fields])
    778914            fill_table_cache(f.rel.to._meta, select, tables, where, db_table, cache_tables_seen, max_depth, cur_depth+1)
     915           
     916def replace_by_dict(string, dict):
     917    """
     918    This will take a dictionary of strings to replace and a string and perform the replacement.
     919     This is a helper function for QSplit.
     920     It takes a dictionary of original->replacements and performs the replacements
     921     in a safe, sane mannor. That is, it won't replace a string twice and it will
     922     start with the largest original strings and work its way downward.
     923    """
     924    if len(dict) == 0: # we have no more strings to replace
     925        return string
    779926
     927    curDict = dict.copy()
     928    keys = curDict.keys()
     929    keys.sort(lambda x,y: cmp(len(y), len(x))) # sort the keys by length
     930    key = keys[0]
     931    val = curDict[key]
     932       
     933    del curDict[key]
     934       
     935        offset  = 0
     936        strings = [string]
     937       
     938       
     939        while strings[-1].find(key) != -1:
     940            loc = strings[-1].find(key)
     941            strings.append(strings[-1][len(key) + loc:])
     942            strings[-2] = strings[-2][:loc]
     943
     944        return val.join([replaceDict(s,curDict) for s in strings])
     945
     946                                                                       
     947
     948
    780949def parse_lookup(kwarg_items, opts):
    781950    # Helper function that handles converting API kwargs
    782951    # (e.g. "name__exact": "tom") to SQL.
Back to Top