Index: db/models/query.py
===================================================================
--- db/models/query.py	(revision 4697)
+++ db/models/query.py	(working copy)
@@ -636,10 +636,42 @@
 
 class QOperator(object):
     "Base class for QAnd and QOr"
+    underneath_an_or = False # used to track ORs
+    checked_or       = False
+    
+    def search_for_or(self):
+        " Returns true if an or exists beneath this Q operator. "
+        if type(self) == QOr:
+            return True
+
+        for val in self.args:
+            if val.search_for_or():
+                return True
+
+        return False # there was no OR below this.
+
+    def set_or_found(self, or_value):
+        if self.checked_or:
+            return
+
+        self.underneath_an_or = or_value
+        self.checked_or       = True
+
+        for val in self.args:
+            val.set_or_found(or_value) # set for all the children
+        
     def __init__(self, *args):
         self.args = args
 
     def get_sql(self, opts):
+
+        if not self.checked_or:
+            found_an_or = self.search_for_or()
+            self.set_or_found(found_an_or)
+
+        # now that we've finished building the query
+        self.checked_or = False
+
         joins, where, params = SortedDict(), [], []
         for val in self.args:
             try:
@@ -684,6 +716,11 @@
 
 class Q(object):
     "Encapsulates queries as objects that can be combined logically."
+    underneath_an_or = False
+    checked_or       = False
+    outer_join       = 'LEFT OUTER JOIN'
+    inner_join       = 'INNER JOIN'
+    
     def __init__(self, **kwargs):
         self.kwargs = kwargs
 
@@ -693,15 +730,114 @@
     def __or__(self, other):
         return QOr(self, other)
 
+    def search_for_or(self):
+        ' Returns false, since there are no OR Qs below. '
+        return False
+
+    def set_or_found(self, or_found):
+        """ Set whether or not we have an OR above us. """
+        if not self.checked_or:
+            self.underneath_an_or = or_found
+            self.checked_or       = True  # we checked for an OR
+
     def get_sql(self, opts):
-        return parse_lookup(self.kwargs.items(), opts)
+        # we will check to see if it's an OR, and change the join
+        # based upon that
 
+        if self.underneath_an_or:
+            join_text = self.outer_join
+        else:
+            join_text = self.inner_join
+
+        joins, where, params = parse_lookup(self.kwargs.items(), opts)
+
+        joins2 = {}
+        for item, key in joins.items():
+            # now we will fix the joins dictionary with
+            # the new type of join
+            joins2[item] = (key[0], join_text, key[2])
+
+        # now that we've done creating the query,
+        # it's best that we let ourselves search again
+        self.checked_or = False
+
+        return joins2, where, params
+
+class QSplit(Q):
+    """
+    Encapsulates a single JOIN-type query into one object.
+    This allows you to run an AND on a m2m query:
+      If Article has a m2m relation with Tag and you run:
+        QSplit(Q(tag__value = 'A')) & QSplit(Q(tag__value = 'B'))
+        The above will filter for all articles with a tag with value
+        'A' *and* a tag with a value 'B'.
+    """
+
+    def __init__(self, q):
+        " Creates a separate un-joinable Q from the Q passed in. "
+        self.q = q
+
+    def search_for_or(self):
+        ' Search for an OR beneath this QNot. '
+        return self.q.search_for_or()
+
+    def set_or_found(self, or_found):
+        """ Set whether or not we have an OR above us. """
+        if not self.checked_or:
+            self.underneath_an_or = or_found
+            self.checked_or       = True  # we checked for an OR
+            
+            self.q.set_or_found(or_found) # now we have to make the change to our child
+    
+    def get_sql(self, opts):
+        from django.conf import settings
+        # the quote below depends on the database engine
+        quote = settings.DATABASE_ENGINE == 'mysql' and "`" or '"'
+
+        joins, where, params = self.q.get_sql(opts)
+        
+        key_replace = {}
+        joins2      = SortedDict()
+        where2      = []
+
+        UNIQUE_HASH = hash(self) # this is the unique key
+
+        for key, val in joins.items():
+            cur_key = key.strip(quote) # set the current key
+            cur_val = '%s__%s' % (cur_key, UNIQUE_HASH)
+
+            key_replace[cur_key] = cur_val # add to the list of thigs to replace
+
+            joins2['%s%s%s' % (quote, cur_val, quote)] = val
+
+        # we need 2 passes since now we need to go back and replace any
+        # leftover table names
+        for key, val in joins2.items():
+            joins2[key] = (val[0],val[1],replace_by_dict(val[2], key_replace))
+
+        where2 = [replace_by_dict(clause, key_replace) for clause in where]
+
+        return joins2, where2, params
+        
+        
 class QNot(Q):
     "Encapsulates NOT (...) queries as objects"
     def __init__(self, q):
         "Creates a negation of the q object passed in."
         self.q = q
 
+    def search_for_or(self):
+        ' Search for an OR beneath this QNot. '
+        return self.q.search_for_or()
+
+    def set_or_found(self, or_found):
+        """ Set whether or not we have an OR above us. """
+        if not self.checked_or:
+            self.underneath_an_or = or_found
+            self.checked_or       = True  # we checked for an OR
+            
+            self.q.set_or_found(or_found) # now we have to make the change to our child
+
     def get_sql(self, opts):
         try:
             joins, where, params = self.q.get_sql(opts)
@@ -776,7 +912,40 @@
                 (qn(old_prefix), qn(f.column), qn(db_table), qn(f.rel.get_related_field().column)))
             select.extend(['%s.%s' % (qn(db_table), qn(f2.column)) for f2 in f.rel.to._meta.fields])
             fill_table_cache(f.rel.to._meta, select, tables, where, db_table, cache_tables_seen, max_depth, cur_depth+1)
+            
+def replace_by_dict(string, dict):
+    """
+    This will take a dictionary of strings to replace and a string and perform the replacement.
+     This is a helper function for QSplit.
+     It takes a dictionary of original->replacements and performs the replacements
+     in a safe, sane mannor. That is, it won't replace a string twice and it will
+     start with the largest original strings and work its way downward.
+    """
+    if len(dict) == 0: # we have no more strings to replace
+        return string
 
+    curDict = dict.copy()
+    keys = curDict.keys()
+    keys.sort(lambda x,y: cmp(len(y), len(x))) # sort the keys by length
+    key = keys[0]
+    val = curDict[key]
+        
+    del curDict[key]
+        
+        offset  = 0
+        strings = [string]
+        
+        
+        while strings[-1].find(key) != -1:
+            loc = strings[-1].find(key)
+            strings.append(strings[-1][len(key) + loc:])
+            strings[-2] = strings[-2][:loc]
+
+        return val.join([replaceDict(s,curDict) for s in strings])
+
+                                                                        
+
+
 def parse_lookup(kwarg_items, opts):
     # Helper function that handles converting API kwargs
     # (e.g. "name__exact": "tom") to SQL.
