Django

Code

Ticket #3691: ticket_3691_axiak.diff

File ticket_3691_axiak.diff, 7.2 kB (added by axiak@mit.edu, 2 years ago)

Patch to django for QSplit

  • db/models/query.py

    old new  
    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.