summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES5
-rw-r--r--doc/build/reference/sqlalchemy/expressions.rst4
-rw-r--r--doc/build/reference/sqlalchemy/interfaces.rst2
-rw-r--r--lib/sqlalchemy/types.py52
-rw-r--r--test/aaa_profiling/test_compiler.py2
-rw-r--r--test/sql/test_types.py7
6 files changed, 55 insertions, 17 deletions
diff --git a/CHANGES b/CHANGES
index 3a1376b32..f49c61392 100644
--- a/CHANGES
+++ b/CHANGES
@@ -153,7 +153,10 @@ CHANGES
types, before falling back to that of the known type
on the other side of the expression. If the
"fallback" type is compatible (i.e. CHAR from String),
- the literal side will use that. Also part of
+ the literal side will use that. TypeDecorator
+ types override this by default to coerce the "literal"
+ side unconditionally, which can be changed by implementing
+ the coerce_compared_value() method. Also part of
[ticket:1683].
- Made sqlalchemy.sql.expressions.Executable part of public
diff --git a/doc/build/reference/sqlalchemy/expressions.rst b/doc/build/reference/sqlalchemy/expressions.rst
index 84210f04a..98e41fcb8 100644
--- a/doc/build/reference/sqlalchemy/expressions.rst
+++ b/doc/build/reference/sqlalchemy/expressions.rst
@@ -208,4 +208,6 @@ SQL functions which are known to SQLAlchemy with regards to database-specific re
.. automodule:: sqlalchemy.sql.functions
:members:
:undoc-members:
- :show-inheritance: \ No newline at end of file
+ :show-inheritance:
+
+
diff --git a/doc/build/reference/sqlalchemy/interfaces.rst b/doc/build/reference/sqlalchemy/interfaces.rst
index c629c3250..f9f60882c 100644
--- a/doc/build/reference/sqlalchemy/interfaces.rst
+++ b/doc/build/reference/sqlalchemy/interfaces.rst
@@ -4,3 +4,5 @@ Interfaces
.. automodule:: sqlalchemy.interfaces
:members:
:undoc-members:
+
+
diff --git a/lib/sqlalchemy/types.py b/lib/sqlalchemy/types.py
index fc9ad2d53..5c4e2ca3f 100644
--- a/lib/sqlalchemy/types.py
+++ b/lib/sqlalchemy/types.py
@@ -119,7 +119,7 @@ class AbstractType(Visitable):
def _coerce_compared_value(self, op, value):
_coerced_type = type_map.get(type(value), NULLTYPE)
- if _coerced_type._type_affinity == self._type_affinity:
+ if _coerced_type is NULLTYPE or _coerced_type._type_affinity is self._type_affinity:
return self
else:
return _coerced_type
@@ -281,12 +281,10 @@ class TypeDecorator(AbstractType):
The expression system does the right thing by not attempting to
coerce the "date()" value into an integer-oriented bind parameter.
- However, suppose "somecol" is a ``TypeDecorator`` that is wrapping
- an ``Integer``, and our ``TypeDecorator`` is actually storing dates
- as an "epoch", i.e. a total number of days from a fixed starting
- date. So in this case, we *do* want the expression system to wrap
- the date() into our ``TypeDecorator`` type's system of coercing
- dates into integers. So we would want to define::
+ However, in the case of ``TypeDecorator``, we are usually changing
+ an incoming Python type to something new - ``TypeDecorator`` by
+ default will "coerce" the non-typed side to be the same type as itself.
+ Such as below, we define an "epoch" type that stores a date value as an integer::
class MyEpochType(types.TypeDecorator):
impl = types.Integer
@@ -298,12 +296,20 @@ class TypeDecorator(AbstractType):
def process_result_value(self, value, dialect):
return self.epoch + timedelta(days=value)
-
- def coerce_compared_value(self, op, value):
- if isinstance(value, datetime.date):
- return self
- else:
- raise ValueError("Python date expected.")
+
+ Our expression of ``somecol + date`` with the above type will coerce the
+ "date" on the right side to also be treated as ``MyEpochType``.
+
+ This behavior can be overridden via the :meth:`~TypeDecorator.coerce_compared_value`
+ method, which returns a type that should be used for the value of the expression.
+ Below we set it such that an integer value will be treated as an ``Integer``,
+ and any other value is assumed to be a date and will be treated as a ``MyEpochType``::
+
+ def coerce_compared_value(self, op, value):
+ if isinstance(value, int):
+ return Integer()
+ else:
+ return self
"""
@@ -408,7 +414,22 @@ class TypeDecorator(AbstractType):
return self.impl.result_processor(dialect, coltype)
def coerce_compared_value(self, op, value):
- return self.impl._coerce_compared_value(op, value)
+ """Suggest a type for a 'coerced' Python value in an expression.
+
+ By default, returns self. This method is called by
+ the expression system when an object using this type is
+ on the left or right side of an expression against a plain Python
+ object which does not yet have a SQLAlchemy type assigned::
+
+ expr = table.c.somecolumn + 35
+
+ Where above, if ``somecolumn`` uses this type, this method will
+ be called with the value ``operator.add``
+ and ``35``. The return value is whatever SQLAlchemy type should
+ be used for ``35`` for this particular operation.
+
+ """
+ return self
def _coerce_compared_value(self, op, value):
return self.coerce_compared_value(op, value)
@@ -1547,6 +1568,9 @@ class Interval(_DateAffinity, TypeDecorator):
def _type_affinity(self):
return Interval
+ def _coerce_compared_value(self, op, value):
+ return self.impl._coerce_compared_value(op, value)
+
class FLOAT(Float):
"""The SQL FLOAT type."""
diff --git a/test/aaa_profiling/test_compiler.py b/test/aaa_profiling/test_compiler.py
index e49a3df7f..dc02cece4 100644
--- a/test/aaa_profiling/test_compiler.py
+++ b/test/aaa_profiling/test_compiler.py
@@ -33,7 +33,7 @@ class CompileTest(TestBase, AssertsExecutionResults):
def test_update(self):
t1.update().compile()
- @profiling.function_call_count(122, {'2.4': 81, '3':132})
+ @profiling.function_call_count(129, {'2.4': 81, '3':132})
def test_update_whereclause(self):
t1.update().where(t1.c.c2==12).compile()
diff --git a/test/sql/test_types.py b/test/sql/test_types.py
index d7f0d1ed7..3e9242481 100644
--- a/test/sql/test_types.py
+++ b/test/sql/test_types.py
@@ -869,6 +869,13 @@ class ExpressionTest(TestBase, AssertsExecutionResults, AssertsCompiledSQL):
# is an Integer
expr = column("foo", MyFoobarType) + 5
assert expr.right.type._type_affinity is types.Integer
+
+ # untyped bind - it gets assigned MyFoobarType
+ expr = column("foo", MyFoobarType) + bindparam("foo")
+ assert expr.right.type._type_affinity is MyFoobarType
+
+ expr = column("foo", MyFoobarType) + bindparam("foo", type_=Integer)
+ assert expr.right.type._type_affinity is types.Integer
# unknown type + unknown, right hand bind
# coerces to the left