]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- More fixes to SQLite "join rewriting"; the fix from :ticket:`2967`
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 20 Feb 2014 21:14:29 +0000 (16:14 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 20 Feb 2014 21:14:29 +0000 (16:14 -0500)
implemented right before the release of 0.9.3 affected the case where
a UNION contained nested joins in it.   "Join rewriting" is a feature
with a wide range of possibilities and is the first intricate
"SQL rewriting" feature we've introduced in years, so we're sort of
going through a lot of iterations with it (not unlike eager loading
back in the 0.2/0.3 series, polymorphic loading in 0.4/0.5). We should
be there soon so thanks for bearing with us :).
fixes #2969   re: #2967
- solve the issue of join rewriting inspecting various types of
from objects without using isinstance(), by adding some new
underscored inspection flags to the FromClause hierarchy.

doc/build/changelog/changelog_09.rst
lib/sqlalchemy/__init__.py
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/selectable.py
test/sql/test_join_rewriting.py

index 235b5c4e5a2a779db706af585215d3a6c73e4d2b..5a94e5759680cb16cc7a1896cfc381dda72c0f76 100644 (file)
     .. include:: changelog_07.rst
         :start-line: 5
 
+.. changelog::
+    :version: 0.9.4
+
+    .. change::
+        :tags: orm, bug, sqlite
+        :tickets: 2969
+
+        More fixes to SQLite "join rewriting"; the fix from :ticket:`2967`
+        implemented right before the release of 0.9.3 affected the case where
+        a UNION contained nested joins in it.   "Join rewriting" is a feature
+        with a wide range of possibilities and is the first intricate
+        "SQL rewriting" feature we've introduced in years, so we're sort of
+        going through a lot of iterations with it (not unlike eager loading
+        back in the 0.2/0.3 series, polymorphic loading in 0.4/0.5). We should
+        be there soon so thanks for bearing with us :).
+
+
 .. changelog::
     :version: 0.9.3
     :released: February 19, 2014
index da0c2103cc71eb29dbe79131f81b5a6c17bb36ad..67155e0f2b502e31d868e6d06eb050e561889672 100644 (file)
@@ -116,7 +116,7 @@ from .schema import (
 from .inspection import inspect
 from .engine import create_engine, engine_from_config
 
-__version__ = '0.9.3'
+__version__ = '0.9.4'
 
 def __go(lcls):
     global __all__
index 148da19aa91bc0db569cc7fd160d3d792cd946a4..5165ee78f160706932ce35da80904b84773102e6 100644 (file)
@@ -1280,13 +1280,7 @@ class SQLCompiler(Compiled):
         cloned = {}
         column_translate = [{}]
 
-        # TODO: should we be using isinstance() for this,
-        # as this whole system won't work for custom Join/Select
-        # subclasses where compilation routines
-        # call down to compiler.visit_join(), compiler.visit_select()
-        join_name = selectable.Join.__visit_name__
-        select_name = selectable.Select.__visit_name__
-        alias_name = selectable.Alias.__visit_name__
+
         def visit(element, **kw):
             if element in column_translate[-1]:
                 return column_translate[-1][element]
@@ -1296,7 +1290,7 @@ class SQLCompiler(Compiled):
 
             newelem = cloned[element] = element._clone()
 
-            if newelem.__visit_name__ is join_name and \
+            if newelem.is_selectable and newelem._is_join and \
                 isinstance(newelem.right, selectable.FromGrouping):
 
                 newelem._reset_exported()
@@ -1340,11 +1334,22 @@ class SQLCompiler(Compiled):
                 newelem.right = selectable_
 
                 newelem.onclause = visit(newelem.onclause, **kw)
-            elif newelem.__visit_name__ is alias_name \
-                and newelem.element.__visit_name__ is select_name:
-                column_translate.append({})
+
+            elif newelem.is_selectable and newelem._is_from_container:
+                # if we hit an Alias or CompoundSelect, put a marker in the
+                # stack.
+                kw['transform_clue'] = 'select_container'
+                newelem._copy_internals(clone=visit, **kw)
+            elif newelem.is_selectable and newelem._is_select:
+                barrier_select = kw.get('transform_clue', None) == 'select_container'
+                # if we're still descended from an Alias/CompoundSelect, we're
+                # in a FROM clause, so start with a new translate collection
+                if barrier_select:
+                    column_translate.append({})
+                kw['transform_clue'] = 'inside_select'
                 newelem._copy_internals(clone=visit, **kw)
-                del column_translate[-1]
+                if barrier_select:
+                    del column_translate[-1]
             else:
                 newelem._copy_internals(clone=visit, **kw)
 
index bda3d655e68458dd84a98100f61eefcf4ffd8427..59d6687b54625d43115f7d932a65a2bfabf501d7 100644 (file)
@@ -137,6 +137,10 @@ class FromClause(Selectable):
     named_with_column = False
     _hide_froms = []
 
+    _is_join = False
+    _is_select = False
+    _is_from_container = False
+
     _textual = False
     """a marker that allows us to easily distinguish a :class:`.TextAsFrom`
     or similar object from other kinds of :class:`.FromClause` objects."""
@@ -504,6 +508,8 @@ class Join(FromClause):
     """
     __visit_name__ = 'join'
 
+    _is_join = True
+
     def __init__(self, left, right, onclause=None, isouter=False):
         """Construct a new :class:`.Join`.
 
@@ -910,6 +916,8 @@ class Alias(FromClause):
     __visit_name__ = 'alias'
     named_with_column = True
 
+    _is_from_container = True
+
     def __init__(self, selectable, name=None):
         baseselectable = selectable
         while isinstance(baseselectable, Alias):
@@ -1716,6 +1724,8 @@ class CompoundSelect(GenerativeSelect):
     INTERSECT = util.symbol('INTERSECT')
     INTERSECT_ALL = util.symbol('INTERSECT ALL')
 
+    _is_from_container = True
+
     def __init__(self, keyword, *selects, **kwargs):
         self._auto_correlate = kwargs.pop('correlate', False)
         self.keyword = keyword
@@ -1982,6 +1992,7 @@ class Select(HasPrefixes, GenerativeSelect):
     _correlate = ()
     _correlate_except = None
     _memoized_property = SelectBase._memoized_property
+    _is_select = True
 
     def __init__(self,
                 columns=None,
index cf9878956e878441c582ed25d2e2766674648fad..082b6f0dea98c9c6d695c3be14fa160009056dee 100644 (file)
@@ -1,4 +1,4 @@
-from sqlalchemy import Table, Column, Integer, MetaData, ForeignKey, select, exists
+from sqlalchemy import Table, Column, Integer, MetaData, ForeignKey, select, exists, union
 from sqlalchemy.testing import fixtures, AssertsCompiledSQL, eq_
 from sqlalchemy import util
 from sqlalchemy.engine import default
@@ -16,6 +16,16 @@ b = Table('b', m,
         Column('a_id', Integer, ForeignKey('a.id'))
     )
 
+b1 = Table('b1', m,
+        Column('id', Integer, primary_key=True),
+        Column('a_id', Integer, ForeignKey('a.id'))
+    )
+
+b2 = Table('b2', m,
+        Column('id', Integer, primary_key=True),
+        Column('a_id', Integer, ForeignKey('a.id'))
+    )
+
 a_to_b = Table('a_to_b', m,
         Column('a_id', Integer, ForeignKey('a.id')),
         Column('b_id', Integer, ForeignKey('b.id')),
@@ -174,6 +184,23 @@ class _JoinRewriteTestBase(AssertsCompiledSQL):
             self._a_atobalias_balias
         )
 
+    def test_b_ab1_union_b_ab2(self):
+        j1 = a.join(b1)
+        j2 = a.join(b2)
+
+        b_j1 = b.join(j1)
+        b_j2 = b.join(j2)
+
+        s = union(
+                select([b_j1], use_labels=True),
+                select([b_j2], use_labels=True)
+            ).select(use_labels=True)
+
+        self._test(
+            s,
+            self._b_ab1_union_c_ab2
+        )
+
 
 class JoinRewriteTest(_JoinRewriteTestBase, fixtures.TestBase):
     """test rendering of each join with right-nested rewritten as
@@ -279,6 +306,20 @@ class JoinRewriteTest(_JoinRewriteTestBase, fixtures.TestBase):
         "JOIN b AS b_1 ON b_1.id = a_to_b_1.b_id) AS anon_1 ON a.id = anon_1.a_to_b_1_a_id"
     )
 
+    _b_ab1_union_c_ab2 = (
+        "SELECT b_id AS b_id, b_a_id AS b_a_id, a_id AS a_id, b1_id AS b1_id, "
+        "b1_a_id AS b1_a_id FROM "
+        "(SELECT b.id AS b_id, b.a_id AS b_a_id, anon_1.a_id AS a_id, "
+            "anon_1.b1_id AS b1_id, anon_1.b1_a_id AS b1_a_id "
+            "FROM b JOIN (SELECT a.id AS a_id, b1.id AS b1_id, b1.a_id AS b1_a_id "
+            "FROM a JOIN b1 ON a.id = b1.a_id) AS anon_1 ON anon_1.a_id = b.a_id "
+        "UNION "
+        "SELECT b.id AS b_id, b.a_id AS b_a_id, anon_2.a_id AS a_id, "
+        "anon_2.b2_id AS b2_id, anon_2.b2_a_id AS b2_a_id "
+        "FROM b JOIN (SELECT a.id AS a_id, b2.id AS b2_id, b2.a_id AS b2_a_id "
+        "FROM a JOIN b2 ON a.id = b2.a_id) AS anon_2 ON anon_2.a_id = b.a_id)"
+    )
+
 class JoinPlainTest(_JoinRewriteTestBase, fixtures.TestBase):
     """test rendering of each join with normal nesting."""
     @util.classproperty
@@ -360,6 +401,18 @@ class JoinPlainTest(_JoinRewriteTestBase, fixtures.TestBase):
         "JOIN b AS b_1 ON b_1.id = a_to_b_1.b_id) ON a.id = a_to_b_1.a_id"
     )
 
+    _b_ab1_union_c_ab2 = (
+        "SELECT b_id AS b_id, b_a_id AS b_a_id, a_id AS a_id, b1_id AS b1_id, "
+        "b1_a_id AS b1_a_id FROM "
+        "(SELECT b.id AS b_id, b.a_id AS b_a_id, a.id AS a_id, b1.id AS b1_id, "
+        "b1.a_id AS b1_a_id FROM b "
+        "JOIN (a JOIN b1 ON a.id = b1.a_id) ON a.id = b.a_id "
+        "UNION "
+        "SELECT b.id AS b_id, b.a_id AS b_a_id, a.id AS a_id, b2.id AS b2_id, "
+        "b2.a_id AS b2_a_id FROM b "
+        "JOIN (a JOIN b2 ON a.id = b2.a_id) ON a.id = b.a_id)"
+    )
+
 class JoinNoUseLabelsTest(_JoinRewriteTestBase, fixtures.TestBase):
     @util.classproperty
     def __dialect__(cls):
@@ -442,12 +495,23 @@ class JoinNoUseLabelsTest(_JoinRewriteTestBase, fixtures.TestBase):
         "JOIN b AS b_1 ON b_1.id = a_to_b_1.b_id) ON a.id = a_to_b_1.a_id"
     )
 
+    _b_ab1_union_c_ab2 = (
+        "SELECT b_id, b_a_id, a_id, b1_id, b1_a_id "
+        "FROM (SELECT b.id AS b_id, b.a_id AS b_a_id, a.id AS a_id, "
+        "b1.id AS b1_id, b1.a_id AS b1_a_id "
+        "FROM b JOIN (a JOIN b1 ON a.id = b1.a_id) ON a.id = b.a_id "
+        "UNION "
+        "SELECT b.id AS b_id, b.a_id AS b_a_id, a.id AS a_id, b2.id AS b2_id, "
+        "b2.a_id AS b2_a_id "
+        "FROM b JOIN (a JOIN b2 ON a.id = b2.a_id) ON a.id = b.a_id)"
+    )
+
 class JoinExecTest(_JoinRewriteTestBase, fixtures.TestBase):
     """invoke the SQL on the current backend to ensure compatibility"""
 
     _a_bc = _a_bc_comma_a1_selbc = _a__b_dc = _a_bkeyassoc = \
         _a_bkeyassoc_aliased = _a_atobalias_balias_c_w_exists = \
-        _a_atobalias_balias = None
+        _a_atobalias_balias = _b_ab1_union_c_ab2 = None
 
     @classmethod
     def setup_class(cls):