--- /dev/null
+.. change::
+ :tags: bug, orm
+ :tickets: 6350
+
+ Fixed two distinct issues, each of which would come into play under certain
+ circumstances, most likely however one which is a common mis-configuration
+ in :class:`_hybrid.hybrid_property`, where the "expression" implementation
+ would return a non :class:`_sql.ClauseElement` such as a boolean value.
+ For both issues, 1.3's behavior was to silently ignore the
+ mis-configuration and ultimately attempt to interpret the value as a
+ SQL expression, which would lead to an incorrect query.
+
+ * Fixed issue regarding interaction of the attribute system with
+ hybrid_property, where if the ``__clause_element__()`` method of the
+ attribute returned a non-:class:`_sql.ClauseElement` object, an internal
+ ``AttributeError`` would lead the attribute to return the ``expression``
+ function on the hybrid_property itself, as the attribute error was
+ against the name ``.expression`` which would invoke the ``__getattr__()``
+ method as a fallback. This now raises explicitly. In 1.3 the
+ non-:class:`_sql.ClauseElement` was returned directly.
+
+ * Fixed issue in SQL argument coercions system where passing the wrong
+ kind of object to methods that expect column expressions would fail if
+ the object were altogether not a SQLAlchemy object, such as a Python
+ function, in cases where the object were not just coerced into a bound
+ value. Again 1.3 did not have a comprehensive argument coercion system
+ so this case would also pass silently.
+
from .base import SQL_OK # noqa
from .base import state_str
from .. import event
+from .. import exc
from .. import inspection
from .. import util
from ..sql import base as sql_base
"entity_namespace": self._entity_namespace,
}
- return self.comparator.__clause_element__()._annotate(annotations)
+ ce = self.comparator.__clause_element__()
+ try:
+ anno = ce._annotate
+ except AttributeError as ae:
+ util.raise_(
+ exc.InvalidRequestError(
+ 'When interpreting attribute "%s" as a SQL expression, '
+ "expected __clause_element__() to return "
+ "a ClauseElement object, got: %r" % (self, ce)
+ ),
+ from_=ae,
+ )
+ else:
+ return anno(annotations)
@property
def _entity_namespace(self):
def _implicit_coercions(
self, original_element, resolved, argname=None, **kw
):
- if not resolved.is_clause_element:
+ if not getattr(resolved, "is_clause_element", False):
self._raise_for_expected(original_element, argname, resolved)
elif resolved._is_select_statement:
self._warn_for_scalar_subquery_coercion()
from decimal import Decimal
+from sqlalchemy import exc
from sqlalchemy import ForeignKey
from sqlalchemy import func
from sqlalchemy import inspect
from sqlalchemy import Numeric
from sqlalchemy import select
from sqlalchemy import String
+from sqlalchemy import testing
from sqlalchemy.ext import hybrid
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import aliased
return A
+ def _wrong_expr_fixture(self):
+ Base = declarative_base()
+
+ class A(Base):
+ __tablename__ = "a"
+ id = Column(Integer, primary_key=True)
+ _value = Column("value", String)
+
+ @hybrid.hybrid_property
+ def value(self):
+ return self._value is not None
+
+ @value.expression
+ def value(cls):
+ return cls._value is not None
+
+ return A
+
def _relationship_fixture(self):
Base = declarative_base()
A.value.__clause_element__(), "foo(a.value) + bar(a.value)"
)
+ def test_expression_isnt_clause_element(self):
+ A = self._wrong_expr_fixture()
+
+ from sqlalchemy.sql import coercions, roles
+
+ with testing.expect_raises_message(
+ exc.InvalidRequestError,
+ 'When interpreting attribute "A.value" as a SQL expression, '
+ r"expected __clause_element__\(\) to return a "
+ "ClauseElement object, got: True",
+ ):
+ coercions.expect(roles.ExpressionElementRole, A.value)
+
def test_any(self):
A, B = self._relationship_fixture()
sess = fixture_session()
):
expect(roles.ExpressionElementRole, t.select().alias())
+ def test_raise_on_regular_python_obj_for_expr(self):
+ """test #6350"""
+
+ def some_function():
+ pass
+
+ class Thing(object):
+ def __clause_element__(self):
+ return some_function
+
+ with testing.expect_raises_message(
+ exc.ArgumentError,
+ r"SQL expression element expected, got "
+ "<function .*some_function .* resolved from <.*Thing object .*",
+ ):
+ expect(roles.ExpressionElementRole, Thing())
+
def test_statement_text_coercion(self):
with testing.expect_deprecated_20(
"Using plain strings to indicate SQL statements"