Commits

Mike Bayer committed 574dba8

Fixed bug where the expression system relied upon the ``str()``
form of a some expressions when referring to the ``.c`` collection
on a ``select()`` construct, but the ``str()`` form isn't available
since the element relies on dialect-specific compilation constructs,
notably the ``__getitem__()`` operator as used with a Postgresql
``ARRAY`` element. The fix also adds a new exception class
:class:`.UnsupportedCompilationError` which is raised in those cases
where a compiler is asked to compile something it doesn't know
how to.
[ticket:2780]

Comments (0)

Files changed (7)

doc/build/changelog/changelog_08.rst

 .. changelog::
     :version: 0.8.3
 
+    .. change::
+        :tags: bug, sql, postgresql
+        :tickets: 2780
+
+        Fixed bug where the expression system relied upon the ``str()``
+        form of a some expressions when referring to the ``.c`` collection
+        on a ``select()`` construct, but the ``str()`` form isn't available
+        since the element relies on dialect-specific compilation constructs,
+        notably the ``__getitem__()`` operator as used with a Postgresql
+        ``ARRAY`` element.  The fix also adds a new exception class
+        :class:`.UnsupportedCompilationError` which is raised in those cases
+        where a compiler is asked to compile something it doesn't know
+        how to.
+
+    .. change::
+        :tags: bug, engine, oracle
         :tickets: 2776
 
         Dialect.initialize() is not called a second time if an :class:`.Engine`

lib/sqlalchemy/exc.py

 class CompileError(SQLAlchemyError):
     """Raised when an error occurs during SQL compilation"""
 
+class UnsupportedCompilationError(CompileError):
+    """Raised when an operation is not supported by the given compiler.
+
+
+    .. versionadded:: 0.8.3
+
+    """
+
+    def __init__(self, compiler, element_type):
+        super(UnsupportedCompilationError, self).__init__(
+                    "Compiler %r can't render element of type %s" %
+                                (compiler, element_type))
 
 class IdentifierError(SQLAlchemyError):
     """Raised when a schema name is beyond the max character limit"""

lib/sqlalchemy/sql/compiler.py

         if disp:
             return disp(binary, operator, **kw)
         else:
-            return self._generate_generic_binary(binary,
-                                OPERATORS[operator], **kw)
+            try:
+                opstring = OPERATORS[operator]
+            except KeyError:
+                raise exc.UnsupportedCompilationError(self, operator)
+            else:
+                return self._generate_generic_binary(binary, opstring, **kw)
 
     def visit_custom_op_binary(self, element, operator, **kw):
         return self._generate_generic_binary(element,

lib/sqlalchemy/sql/expression.py

         """
         if name is None:
             name = self.anon_label
-            key = str(self)
+            try:
+                key = str(self)
+            except exc.UnsupportedCompilationError:
+                key = self.anon_label
         else:
             key = name
         co = ColumnClause(_as_truncated(name) if name_is_truncatable else name,

lib/sqlalchemy/sql/visitors.py

 from collections import deque
 from .. import util
 import operator
+from .. import exc
 
 __all__ = ['VisitableType', 'Visitable', 'ClauseVisitor',
     'CloningVisitor', 'ReplacingCloningVisitor', 'iterate',
             getter = operator.attrgetter("visit_%s" % visit_name)
 
             def _compiler_dispatch(self, visitor, **kw):
-                return getter(visitor)(self, **kw)
+                try:
+                    meth = getter(visitor)
+                except AttributeError:
+                    raise exc.UnsupportedCompilationError(visitor, cls)
+                else:
+                    return meth(self, **kw)
         else:
             # The optimization opportunity is lost for this case because the
             # __visit_name__ is not yet a string. As a result, the visit
             # string has to be recalculated with each compilation.
             def _compiler_dispatch(self, visitor, **kw):
                 visit_attr = 'visit_%s' % self.__visit_name__
-                return getattr(visitor, visit_attr)(self, **kw)
+                try:
+                    meth = getattr(visitor, visit_attr)
+                except AttributeError:
+                    raise exc.UnsupportedCompilationError(visitor, cls)
+                else:
+                    return meth(self, **kw)
 
         _compiler_dispatch.__doc__ = \
           """Look for an attribute named "visit_" + self.__visit_name__

test/sql/test_compiler.py

         )
 
 
+class UnsupportedTest(fixtures.TestBase):
+    def test_unsupported_element_str_visit_name(self):
+        from sqlalchemy.sql.expression import ClauseElement
+        class SomeElement(ClauseElement):
+            __visit_name__ = 'some_element'
+
+        assert_raises_message(
+            exc.UnsupportedCompilationError,
+            r"Compiler <sqlalchemy.sql.compiler.SQLCompiler .*"
+            r"can't render element of type <class '.*SomeElement'>",
+            SomeElement().compile
+        )
+
+    def test_unsupported_element_meth_visit_name(self):
+        from sqlalchemy.sql.expression import ClauseElement
+        class SomeElement(ClauseElement):
+            @classmethod
+            def __visit_name__(cls):
+                return "some_element"
+
+        assert_raises_message(
+            exc.UnsupportedCompilationError,
+            r"Compiler <sqlalchemy.sql.compiler.SQLCompiler .*"
+            r"can't render element of type <class '.*SomeElement'>",
+            SomeElement().compile
+        )
+
+    def test_unsupported_operator(self):
+        from sqlalchemy.sql.expression import BinaryExpression
+        def myop(x, y):
+            pass
+        binary = BinaryExpression(column("foo"), column("bar"), myop)
+        assert_raises_message(
+            exc.UnsupportedCompilationError,
+            r"Compiler <sqlalchemy.sql.compiler.SQLCompiler .*"
+            r"can't render element of type <function.*",
+            binary.compile
+        )
+
+
 class KwargPropagationTest(fixtures.TestBase):
 
     @classmethod

test/sql/test_selectable.py

         s = select([t])._clone()
         assert c in s.c.bar.proxy_set
 
+
+    def test_no_error_on_unsupported_expr_key(self):
+        from sqlalchemy.dialects.postgresql import ARRAY
+
+        t = table('t', column('x', ARRAY(Integer)))
+
+        expr = t.c.x[5]
+        s = select([t, expr])
+        eq_(
+            s.c.keys(),
+            ['x', expr.anon_label]
+        )
+
     def test_cloned_intersection(self):
         t1 = table('t1', column('x'))
         t2 = table('t2', column('x'))