.. changelog::
:version: 0.8.2
+ .. change::
+ :tags: feature, sql
+ :tickets: 2744, 2734
+
+ Provided a new attribute for :class:`.TypeDecorator`
+ called :attr:`.TypeDecorator.coerce_to_is_types`,
+ to make it easier to control how comparisons using
+ ``==`` or ``!=`` to ``None`` and boolean types goes
+ about producing an ``IS`` expression, or a plain
+ equality expression with a bound parameter.
+
.. change::
:tags: feature, postgresql
return op, other_comparator.type
def _boolean_compare(self, expr, op, obj, negate=None, reverse=False,
+ _python_is_types=(util.NoneType, bool),
**kwargs):
- if isinstance(obj, (util.NoneType, bool, Null, True_, False_)):
+
+ if isinstance(obj, _python_is_types + (Null, True_, False_)):
# allow x ==/!= True/False to be treated as a literal.
# this comes out to "== / != true/false" or "1/0" if those
type_=sqltypes.BOOLEANTYPE,
negate=negate, modifiers=kwargs)
- def _binary_operate(self, expr, op, obj, reverse=False, result_type=None):
+ def _binary_operate(self, expr, op, obj, reverse=False, result_type=None,
+ **kw):
obj = self._check_literal(expr, op, obj)
if reverse:
"type being decorated")
self.impl = to_instance(self.__class__.impl, *args, **kwargs)
+ coerce_to_is_types = (util.NoneType, )
+ """Specify those Python types which should be coerced at the expression
+ level to "IS <constant>" when compared using ``==`` (and same for
+ ``IS NOT`` in conjunction with ``!=``.
+
+ For most SQLAlchemy types, this includes ``NoneType``, as well as ``bool``.
+
+ :class:`.TypeDecorator` modifies this list to only include ``NoneType``,
+ as typedecorator implementations that deal with boolean types are common.
+
+ Custom :class:`.TypeDecorator` classes can override this attribute to
+ return an empty tuple, in which case no values will be coerced to
+ constants.
+
+ ..versionadded:: 0.8.2
+ Added :attr:`.TypeDecorator.coerce_to_is_types` to allow for easier
+ control of ``__eq__()`` ``__ne__()`` operations.
+
+ """
+
+ class Comparator(TypeEngine.Comparator):
+ def operate(self, op, *other, **kwargs):
+ kwargs['_python_is_types'] = self.expr.type.coerce_to_is_types
+ return super(TypeDecorator.Comparator, self).operate(
+ op, *other, **kwargs)
+
+ def reverse_operate(self, op, other, **kwargs):
+ kwargs['_python_is_types'] = self.expr.type.coerce_to_is_types
+ return super(TypeDecorator.Comparator, self).reverse_operate(
+ op, other, **kwargs)
+
@property
def comparator_factory(self):
- return self.impl.comparator_factory
+ return type("TDComparator",
+ (TypeDecorator.Comparator, self.impl.comparator_factory),
+ {})
def _gen_dialect_impl(self, dialect):
"""
expr = column('foo', CHAR) == "asdf"
eq_(expr.right.type.__class__, CHAR)
-
@testing.uses_deprecated
@testing.fails_on('firebird', 'Data type unknown on the parameter')
@testing.fails_on('mssql', 'int is unsigned ? not clear')
"BIND_INfooBIND_INhiBIND_OUT"
)
+ def test_typedec_is_adapt(self):
+ class CoerceNothing(TypeDecorator):
+ coerce_to_is_types = ()
+ impl = Integer
+ class CoerceBool(TypeDecorator):
+ coerce_to_is_types = (bool, )
+ impl = Boolean
+ class CoerceNone(TypeDecorator):
+ coerce_to_is_types = (type(None),)
+ impl = Integer
+
+ c1 = column('x', CoerceNothing())
+ c2 = column('x', CoerceBool())
+ c3 = column('x', CoerceNone())
+
+ self.assert_compile(
+ and_(c1 == None, c2 == None, c3 == None),
+ "x = :x_1 AND x = :x_2 AND x IS NULL"
+ )
+ self.assert_compile(
+ and_(c1 == True, c2 == True, c3 == True),
+ "x = :x_1 AND x = true AND x = :x_2"
+ )
+ self.assert_compile(
+ and_(c1 == 3, c2 == 3, c3 == 3),
+ "x = :x_1 AND x = :x_2 AND x = :x_3"
+ )
+ self.assert_compile(
+ and_(c1.is_(True), c2.is_(True), c3.is_(True)),
+ "x IS :x_1 AND x IS true AND x IS :x_2"
+ )
+
+
def test_typedec_righthand_coercion(self):
class MyTypeDec(types.TypeDecorator):
impl = String