.. changelog::
:version: 1.1.0
+ .. change::
+ :tags: bug, orm
+ :tickets: 3802
+
+ ORM attributes can now be assigned any object that is has a
+ ``__clause_element__()`` attribute, which will result in inline
+ SQL the way any :class:`.ClauseElement` class does. This covers other
+ mapped attributes not otherwise transformed by further expression
+ constructs.
+
.. change::
:tags: feature, orm
:tickets: 3812
The typing system now has specific checks for passing of SQLAlchemy
"inspectable" objects in contexts where they would otherwise be handled as
literal values. Any SQLAlchemy built-in object that is legal to pass as a
-SQL value includes a method ``__clause_element__()`` which provides a
+SQL value (which is not already a :class:`.ClauseElement` instance)
+includes a method ``__clause_element__()`` which provides a
valid SQL expression for that object. For SQLAlchemy objects that
don't provide this, such as mapped classes, mappers, and mapped
instances, a more informative error message is emitted rather than
:ticket:`3809`
-
.. _change_2528:
A UNION or similar of SELECTs with LIMIT/OFFSET/ORDER BY now parenthesizes the embedded selects
def __clause_element__(self):
expr = self.expression
- while hasattr(expr, '__clause_element__'):
+ if hasattr(expr, '__clause_element__'):
expr = expr.__clause_element__()
return expr
col = propkey_to_col[propkey]
if value is None and propkey not in eval_none and not render_nulls:
continue
- elif not bulk and isinstance(value, sql.ClauseElement):
- value_params[col.key] = value
+ elif not bulk and hasattr(value, '__clause_element__') or \
+ isinstance(value, sql.ClauseElement):
+ value_params[col.key] = value.__clause_element__() \
+ if hasattr(value, '__clause_element__') else value
else:
params[col.key] = value
value = state_dict[propkey]
col = propkey_to_col[propkey]
- if isinstance(value, sql.ClauseElement):
- value_params[col] = value
+ if hasattr(value, '__clause_element__') or \
+ isinstance(value, sql.ClauseElement):
+ value_params[col] = value.__clause_element__() \
+ if hasattr(value, '__clause_element__') else value
# guard against values that generate non-__nonzero__
# objects for __eq__()
elif state.manager[propkey].impl.is_equal(
assert 'value' not in hb.__dict__
eq_(hb.value, False)
+ def test_clauseelement_accessor(self):
+ class Thing(object):
+ def __init__(self, value):
+ self.value = value
+
+ def __clause_element__(self):
+ return literal_column(str(self.value))
+
+ User = self.classes.User
+
+ u = User(id=5, name='test', counter=Thing(3))
+
+ session = create_session()
+ session.add(u)
+ session.flush()
+
+ u.counter = Thing(5)
+ session.flush()
+
+ def go():
+ eq_(u.counter, 5)
+ self.sql_count_(1, go)
+
class PassiveDeletesTest(fixtures.MappedTest):
__requires__ = ('foreign_keys',)
is_(inspect(c), c)
assert not c.is_selectable
assert not hasattr(c, 'selectable')
+
+ def test_no_clause_element_on_clauseelement(self):
+ # re [ticket:3802], there are in the wild examples
+ # of looping over __clause_element__, therefore the
+ # absense of __clause_element__ as a test for "this is the clause
+ # element" must be maintained
+
+ x = Column('foo', Integer)
+ assert not hasattr(x, '__clause_element__')
+