]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
don't cache TypeDecorator by default
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 6 May 2021 16:24:00 +0000 (12:24 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 6 May 2021 17:57:43 +0000 (13:57 -0400)
The :class:`.TypeDecorator` class will now emit a warning when used in SQL
compilation with caching unless the ``.cache_ok`` flag is set to ``True``
or ``False``. ``.cache_ok`` indicates that all the parameters passed to the
object are safe to be used as a cache key, ``False`` means they are not.

Fixes: #6436
Change-Id: Ib1bb7dc4b124e38521d615c2e2e691e4915594fb

36 files changed:
doc/build/changelog/unreleased_14/6436.rst [new file with mode: 0644]
doc/build/core/custom_types.rst
lib/sqlalchemy/dialects/mssql/information_schema.py
lib/sqlalchemy/dialects/sqlite/pysqlite.py
lib/sqlalchemy/sql/sqltypes.py
lib/sqlalchemy/sql/traversals.py
lib/sqlalchemy/sql/type_api.py
lib/sqlalchemy/testing/suite/test_types.py
test/dialect/mssql/test_types.py
test/dialect/mysql/test_compiler.py
test/dialect/mysql/test_types.py
test/dialect/oracle/test_compiler.py
test/dialect/oracle/test_types.py
test/dialect/postgresql/test_dialect.py
test/dialect/postgresql/test_on_conflict.py
test/dialect/postgresql/test_types.py
test/dialect/test_sqlite.py
test/engine/test_execute.py
test/ext/mypy/files/type_decorator.py
test/ext/test_mutable.py
test/orm/test_lazy_relations.py
test/orm/test_naturalpks.py
test/orm/test_query.py
test/orm/test_relationships.py
test/orm/test_unitofworkv2.py
test/orm/test_versioning.py
test/sql/test_compare.py
test/sql/test_defaults.py
test/sql/test_metadata.py
test/sql/test_operators.py
test/sql/test_query.py
test/sql/test_resultset.py
test/sql/test_returning.py
test/sql/test_selectable.py
test/sql/test_type_expressions.py
test/sql/test_types.py

diff --git a/doc/build/changelog/unreleased_14/6436.rst b/doc/build/changelog/unreleased_14/6436.rst
new file mode 100644 (file)
index 0000000..e6b83d8
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: bug, sql, regression
+    :tickets: 6436
+
+    The :class:`.TypeDecorator` class will now emit a warning when used in SQL
+    compilation with caching unless the ``.cache_ok`` flag is set to ``True``
+    or ``False``. A new class-level attribute :attr:`.TypeDecorator.cache_ok`
+    may be set which will be used as an indication that all the parameters
+    passed to the object are safe to be used as a cache key if set to ``True``,
+    ``False`` means they are not.
index 31c66b1e6dc8c1f6e108ba81f2455b86e5dbe6b0..1f506f168d4399888e75fb10b838e3baace1f047 100644 (file)
@@ -148,6 +148,7 @@ denormalize::
 
     class TZDateTime(TypeDecorator):
         impl = DateTime
+        cache_ok = True
 
         def process_bind_param(self, value, dialect):
             if value is not None:
@@ -186,6 +187,7 @@ binary in CHAR(16) if desired::
 
         """
         impl = CHAR
+        cache_ok = True
 
         def load_dialect_impl(self, dialect):
             if dialect.name == 'postgresql':
@@ -233,6 +235,8 @@ to/from JSON.   Can be modified to use Python's builtin json encoder::
 
         impl = VARCHAR
 
+        cache_ok = True
+
         def process_bind_param(self, value, dialect):
             if value is not None:
                 value = json.dumps(value)
@@ -306,6 +310,8 @@ method::
 
         impl = VARCHAR
 
+        cache_ok = True
+
         def coerce_compared_value(self, op, value):
             if op in (operators.like_op, operators.not_like_op):
                 return String()
@@ -416,8 +422,11 @@ transparently::
     class PGPString(TypeDecorator):
         impl = BYTEA
 
+        cache_ok = True
+
         def __init__(self, passphrase):
             super(PGPString, self).__init__()
+
             self.passphrase = passphrase
 
         def bind_expression(self, bindvalue):
index c379207974cff1555768bd13bebe273c2b0e4472..eb8f6db5b6a13e4cc61171bc85d8150cd01b93d6 100644 (file)
@@ -25,6 +25,7 @@ ischema = MetaData()
 
 class CoerceUnicode(TypeDecorator):
     impl = Unicode
+    cache_ok = True
 
     def process_bind_param(self, value, dialect):
         if util.py2k and isinstance(value, util.binary_type):
@@ -211,6 +212,7 @@ class IdentitySqlVariant(TypeDecorator):
       correct value as string.
     """
     impl = Unicode
+    cache_ok = True
 
     def column_expression(self, colexpr):
         return cast(colexpr, Numeric)
index 20a4bb7acb2831b030676bc2e1a5a0297e5bde0a..8df0037bf2a4331785d8de40cf2456a2ecd640a2 100644 (file)
@@ -312,6 +312,7 @@ same column, use a custom type that will check each row individually::
 
     class MixedBinary(TypeDecorator):
         impl = String
+        cache_ok = True
 
         def process_result_value(self, value, dialect):
             if isinstance(value, str):
index 024b9f01e72b976a3b9cda3542d2487b06bd5728..4f8654afd9d26245f06e6a48d61ada40bf96adeb 100644 (file)
@@ -1780,6 +1780,7 @@ class PickleType(TypeDecorator):
     """
 
     impl = LargeBinary
+    cache_ok = True
 
     def __init__(
         self, protocol=pickle.HIGHEST_PROTOCOL, pickler=None, comparator=None
@@ -2027,6 +2028,7 @@ class Interval(Emulated, _AbstractInterval, TypeDecorator):
 
     impl = DateTime
     epoch = dt.datetime.utcfromtimestamp(0)
+    cache_ok = True
 
     def __init__(self, native=True, second_precision=None, day_precision=None):
         """Construct an Interval object.
index f2099f191101258041b584141d11e49eb212c6c6..e8a80528585960cd02573af699fdf6591e686371 100644 (file)
@@ -152,7 +152,11 @@ class HasCacheKey(object):
                 # efficient switch construct
 
                 if meth is STATIC_CACHE_KEY:
-                    result += (attrname, obj._static_cache_key)
+                    sck = obj._static_cache_key
+                    if sck is NO_CACHE:
+                        anon_map[NO_CACHE] = True
+                        return None
+                    result += (attrname, sck)
                 elif meth is ANON_NAME:
                     elements = util.preloaded.sql_elements
                     if isinstance(obj, elements._anonymous_label):
index 69cd3c5caf4cc99e289404ecc67c20985aec69e0..47f6c3005245b9cc6efe128be3691150a5fc04ad 100644 (file)
@@ -10,6 +10,7 @@
 """
 
 
+from sqlalchemy.sql.traversals import NO_CACHE
 from . import operators
 from .base import SchemaEventTarget
 from .visitors import Traversible
@@ -872,6 +873,8 @@ class TypeDecorator(SchemaEventTarget, TypeEngine):
 
           impl = types.Unicode
 
+          cache_ok = True
+
           def process_bind_param(self, value, dialect):
               return "PREFIX:" + value
 
@@ -887,6 +890,16 @@ class TypeDecorator(SchemaEventTarget, TypeEngine):
     given; in this case, the ``impl`` variable can reference
     ``TypeEngine`` as a placeholder.
 
+    The :attr:`.TypeDecorator.cache_ok` class-level flag indicates if this
+    custom :class:`.TypeDecorator` is safe to be used as part of a cache key.
+    This flag defaults to ``None`` which will initially generate a warning
+    when the SQL compiler attempts to generate a cache key for a statement
+    that uses this type.  If the :class:`.TypeDecorator` is not guaranteed
+    to produce the same bind/result behavior and SQL generation
+    every time, this flag should be set to ``False``; otherwise if the
+    class produces the same behavior each time, it may be set to ``True``.
+    See :attr:`.TypeDecorator.cache_ok` for further notes on how this works.
+
     Types that receive a Python type that isn't similar to the ultimate type
     used may want to define the :meth:`TypeDecorator.coerce_compared_value`
     method. This is used to give the expression system a hint when coercing
@@ -946,6 +959,8 @@ class TypeDecorator(SchemaEventTarget, TypeEngine):
             class MyJsonType(TypeDecorator):
                 impl = postgresql.JSON
 
+                cache_ok = True
+
                 def coerce_compared_value(self, op, value):
                     return self.impl.coerce_compared_value(op, value)
 
@@ -1002,6 +1017,47 @@ class TypeDecorator(SchemaEventTarget, TypeEngine):
 
     """
 
+    cache_ok = None
+    """Indicate if statements using this :class:`.TypeDecorator` are "safe to
+    cache".
+
+    The default value ``None`` will emit a warning and then not allow caching
+    of a statement which includes this type.   Set to ``False`` to disable
+    statements using this type from being cached at all without a warning.
+    When set to ``True``, the object's class and selected elements from its
+    state will be used as part of the cache key, e.g.::
+
+        class MyType(TypeDecorator):
+            impl = String
+
+            cache_ok = True
+
+            def __init__(self, choices):
+                self.choices = tuple(choices)
+                self.internal_only = True
+
+    The cache key for the above type would be equivalent to::
+
+        (<class '__main__.MyType'>, ('choices', ('a', 'b', 'c')))
+
+    The caching scheme will extract attributes from the type that correspond
+    to the names of parameters in the ``__init__()`` method.  Above, the
+    "choices" attribute becomes part of the cache key but "internal_only"
+    does not, because there is no parameter named "internal_only".
+
+    The requirements for cacheable elements is that they are hashable
+    and also that they indicate the same SQL rendered for expressions using
+    this type every time for a given cache value.
+
+    .. versionadded:: 1.4.14 - added the ``cache_ok`` flag to allow
+       some configurability of caching for :class:`.TypeDecorator` classes.
+
+    .. seealso::
+
+        :ref:`sql_caching`
+
+    """
+
     class Comparator(TypeEngine.Comparator):
         """A :class:`.TypeEngine.Comparator` that is specific to
         :class:`.TypeDecorator`.
@@ -1037,6 +1093,21 @@ class TypeDecorator(SchemaEventTarget, TypeEngine):
                 {},
             )
 
+    @property
+    def _static_cache_key(self):
+        if self.cache_ok is None:
+            util.warn(
+                "TypeDecorator %r will not produce a cache key because "
+                "the ``cache_ok`` flag is not set to True.  "
+                "Set this flag to True if this type object's "
+                "state is safe to use in a cache key, or False to "
+                "disable this warning." % self
+            )
+        elif self.cache_ok is True:
+            return super(TypeDecorator, self)._static_cache_key
+
+        return NO_CACHE
+
     def _gen_dialect_impl(self, dialect):
         """
         #todo
@@ -1465,6 +1536,8 @@ class Variant(TypeDecorator):
 
     """
 
+    cache_ok = True
+
     def __init__(self, base, mapping):
         """Construct a new :class:`.Variant`.
 
index ebcceaae7c06faba434b7baf69ae391b8e2b4ce1..3e54d87a44e62de0bceb217c48ad8588114c7e8c 100644 (file)
@@ -288,6 +288,7 @@ class _DateFixture(_LiteralRoundTripFixture, fixtures.TestBase):
     def define_tables(cls, metadata):
         class Decorated(TypeDecorator):
             impl = cls.datatype
+            cache_ok = True
 
         Table(
             "date_table",
@@ -477,6 +478,7 @@ class CastTypeDecoratorTest(_LiteralRoundTripFixture, fixtures.TestBase):
     def string_as_int(self):
         class StringAsInt(TypeDecorator):
             impl = String(50)
+            cache_ok = True
 
             def get_dbapi_type(self, dbapi):
                 return dbapi.NUMBER
index 798dbfc1fdc8318a370a436868251e7802a23c33..269638cd350bebab944edd2ee292c4697e8f9774 100644 (file)
@@ -1196,6 +1196,7 @@ class StringTest(fixtures.TestBase, AssertsCompiledSQL):
 
 class MyPickleType(types.TypeDecorator):
     impl = PickleType
+    cache_ok = True
 
     def process_bind_param(self, value, dialect):
         if value:
index 84646d3802a96e58af154c154d2fb4e8da10d37f..8d311fb6c8f668692a4a8d9e02d294c0054b6bc9 100644 (file)
@@ -693,6 +693,7 @@ class SQLTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_cast_type_decorator(self):
         class MyInteger(sqltypes.TypeDecorator):
             impl = Integer
+            cache_ok = True
 
         type_ = MyInteger()
         t = sql.table("t", sql.column("col"))
index 355d774ac0c3c72352f53eafd3c2f45869650344..cf39ce4bc0e3fd889e8239a42c0620c800d1b6b9 100644 (file)
@@ -653,6 +653,7 @@ class TypeRoundTripTest(fixtures.TestBase, AssertsExecutionResults):
 
     class MyTime(TypeDecorator):
         impl = TIMESTAMP
+        cache_ok = True
 
     @testing.combinations(
         (TIMESTAMP,),
index e198fa48a92c54d20e63bca7d12609cb05a30003..2bbc14e99a05e4cbd184cd0a0665a5500bbac478 100644 (file)
@@ -568,6 +568,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_limit_preserves_typing_information(self):
         class MyType(TypeDecorator):
             impl = Integer
+            cache_ok = True
 
         stmt = select(type_coerce(column("x"), MyType).label("foo")).limit(1)
         dialect = oracle.dialect()
index f3f8ef7c2d05127e5b3af58f0bdef6f270bfe695..4b119602574e51b59b1e40faad6bcadff08b8382 100644 (file)
@@ -1189,6 +1189,7 @@ class SetInputSizesTest(fixtures.TestBase):
 
         class TestTypeDec(TypeDecorator):
             impl = NullType()
+            cache_ok = True
 
             def load_dialect_impl(self, dialect):
                 if dialect.name == "oracle":
index 1bd947f6a3e558621d3e20ebd973ccc01e91d880..1382788224750cc30f938c3708b3f60d9e67cf31 100644 (file)
@@ -1226,6 +1226,8 @@ $$ LANGUAGE plpgsql;
         class BITD(TypeDecorator):
             impl = Integer
 
+            cache_ok = True
+
             def load_dialect_impl(self, dialect):
                 if dialect.name == "postgresql":
                     return BigInteger()
index 489084de79afaf56ebe6827f9f8856d1368e0e7c..dcf112de6623d5c14f3e10dbee541cceee0a529e 100644 (file)
@@ -42,6 +42,8 @@ class OnConflictTest(fixtures.TablesTest):
         class SpecialType(sqltypes.TypeDecorator):
             impl = String
 
+            cache_ok = True
+
             def process_bind_param(self, value, dialect):
                 return value + " processed"
 
index 2f975e4a8ba57a6bbaa0a20fdcdd6d40927440f3..da550bcb9c308c47a47cb991c2e2ab2e2abea3cd 100644 (file)
@@ -746,6 +746,7 @@ class EnumTest(fixtures.TestBase, AssertsExecutionResults):
     def test_custom_subclass(self, metadata, connection):
         class MyEnum(TypeDecorator):
             impl = Enum("oneHI", "twoHI", "threeHI", name="myenum")
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 if value is not None:
@@ -1391,6 +1392,7 @@ class ArrayRoundTripTest(object):
     def define_tables(cls, metadata):
         class ProcValue(TypeDecorator):
             impl = cls.ARRAY(Integer, dimensions=2)
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 if value is None:
@@ -2127,6 +2129,7 @@ class PGArrayRoundTripTest(
 class _ArrayOfEnum(TypeDecorator):
     # previous workaround for array of enum
     impl = postgresql.ARRAY
+    cache_ok = True
 
     def bind_expression(self, bindvalue):
         return sa.cast(bindvalue, self)
index bae97bb9a2ff6394dfbe83dccc831b16bf926e41..95dbecdc8c5b921e3afa64208987fed5bb501e0d 100644 (file)
@@ -1682,6 +1682,7 @@ class AutoIncrementTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_sqlite_autoincrement_int_affinity(self):
         class MyInteger(sqltypes.TypeDecorator):
             impl = Integer
+            cache_ok = True
 
         table = Table(
             "autoinctable",
@@ -2693,6 +2694,7 @@ class OnConflictTest(fixtures.TablesTest):
 
         class SpecialType(sqltypes.TypeDecorator):
             impl = String
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 return value + " processed"
index f4449383a9e99f4cb2251a13d0229988b6811b94..9ee8d2480c52a1d423fbfe83ff5dedbd6f6c32fc 100644 (file)
@@ -336,6 +336,7 @@ class ExecuteTest(fixtures.TablesTest):
     def test_exception_wrapping_non_dbapi_statement(self):
         class MyType(TypeDecorator):
             impl = Integer
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 raise SomeException("nope")
@@ -539,6 +540,7 @@ class ExecuteTest(fixtures.TablesTest):
 
         class MyType(TypeDecorator):
             impl = Integer
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 raise MyException("nope")
@@ -2575,6 +2577,7 @@ class HandleErrorTest(fixtures.TestBase):
 
         class MyType(TypeDecorator):
             impl = Integer
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 raise nope
index 83f603c63e481ea559eeeb267c5c708df3e54993..07a13caee49bfbb29bdef38322dd740951127dab 100644 (file)
@@ -12,6 +12,7 @@ Base = declarative_base()
 
 class IntToStr(TypeDecorator[int]):
     impl = String
+    cache_ok = True
 
     def process_bind_param(
         self,
index e89107c6d3785716170408f9ec6ff73c064852d4..49d3b9d90a54764630be9f3f88356abe44ee2258 100644 (file)
@@ -970,6 +970,7 @@ class MutableWithScalarJSONTest(_MutableDictTestBase, fixtures.MappedTest):
 
         class JSONEncodedDict(TypeDecorator):
             impl = VARCHAR(50)
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 if value is not None:
@@ -1007,6 +1008,7 @@ class MutableColumnCopyJSONTest(_MutableDictTestBase, fixtures.MappedTest):
 
         class JSONEncodedDict(TypeDecorator):
             impl = VARCHAR(50)
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 if value is not None:
@@ -1188,6 +1190,7 @@ class MutableAssociationScalarJSONTest(
 
         class JSONEncodedDict(TypeDecorator):
             impl = VARCHAR(50)
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 if value is not None:
@@ -1239,6 +1242,7 @@ class CustomMutableAssociationScalarJSONTest(
 
         class JSONEncodedDict(TypeDecorator):
             impl = VARCHAR(50)
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 if value is not None:
index a0b92a28a7624dd8c77c4fe1c008b43b63fdca4a..d75b46886c6cf02ce4a34a39075ee588ebd6cb1d 100644 (file)
@@ -835,9 +835,11 @@ class LazyTest(_fixtures.FixtureTest):
 
         class IntDecorator(TypeDecorator):
             impl = Integer
+            cache_ok = True
 
         class SmallintDecorator(TypeDecorator):
             impl = SmallInteger
+            cache_ok = True
 
         class SomeDBInteger(sa.Integer):
             pass
@@ -986,6 +988,7 @@ class GetterStateTest(_fixtures.FixtureTest):
     def _unhashable_fixture(self, metadata, load_on_pending=False):
         class MyHashType(sa.TypeDecorator):
             impl = sa.String(100)
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 return ";".join(
@@ -1553,6 +1556,7 @@ class TypeCoerceTest(fixtures.MappedTest, testing.AssertsExecutionResults):
 
     class StringAsInt(TypeDecorator):
         impl = String(50)
+        cache_ok = True
 
         def get_dbapi_type(self, dbapi):
             return dbapi.NUMBER
index 6ad612b7404f9f535977a7d9c6d92881c4d76e46..4c2e84208d6eea7617e1e9ca430846674cc0a33b 100644 (file)
@@ -1790,6 +1790,7 @@ class UnsortablePKTest(fixtures.MappedTest):
     def define_tables(cls, metadata):
         class MyUnsortable(TypeDecorator):
             impl = String(10)
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 return "%s,%s" % (value["x"], value["y"])
index d26f94bb885870db4f3453ae7febbbe7909a51b2..7f2a0f71541aab54d9f8f6f9061d3610ab66f781 100644 (file)
@@ -531,6 +531,7 @@ class RowTupleTest(QueryTest):
         class MyType(TypeDecorator):
             impl = Integer
             hashable = False
+            cache_ok = True
 
             def process_result_value(self, value, dialect):
                 return [value]
index a065b4046a4780fb0517525cf534aa8109d56daa..867994866c0961499cadefc922cc1e1d3c40f4a4 100644 (file)
@@ -2692,6 +2692,7 @@ class TypedAssociationTable(fixtures.MappedTest):
     def define_tables(cls, metadata):
         class MySpecialType(sa.types.TypeDecorator):
             impl = String
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 return "lala" + value
index 06ec19e52b4ead1343d4d22d034cceba9328ab40..365a9b7601e6436db22aaffa962ef560a53ca078 100644 (file)
@@ -2803,6 +2803,7 @@ class TypeWoBoolTest(fixtures.MappedTest, testing.AssertsExecutionResults):
 
         class MyType(TypeDecorator):
             impl = String(50)
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 if value is not None:
@@ -2891,6 +2892,8 @@ class NullEvaluatingTest(fixtures.MappedTest, testing.AssertsExecutionResults):
 
         class EvalsNull(TypeDecorator):
             impl = String(50)
+            cache_ok = True
+            cache_ok = True
 
             should_evaluate_none = True
 
index c38434c9c19fe21ed07636803fa0b5a98175e1e7..54500e599d6e14dbfff3c8b46f92ad2e7ef9c58f 100644 (file)
@@ -895,6 +895,7 @@ class ColumnTypeTest(fixtures.MappedTest):
     def define_tables(cls, metadata):
         class SpecialType(TypeDecorator):
             impl = Date
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 assert isinstance(value, datetime.date)
index 21a349d76faa2416616261557e2b8c632cdf921f..257776c506209b0ae892ff20c8a8b62dce61861e 100644 (file)
@@ -23,6 +23,7 @@ from sqlalchemy import table
 from sqlalchemy import testing
 from sqlalchemy import text
 from sqlalchemy import tuple_
+from sqlalchemy import TypeDecorator
 from sqlalchemy import union
 from sqlalchemy import union_all
 from sqlalchemy import util
@@ -74,6 +75,7 @@ from sqlalchemy.testing import is_false
 from sqlalchemy.testing import is_not
 from sqlalchemy.testing import is_true
 from sqlalchemy.testing import ne_
+from sqlalchemy.testing.assertions import expect_warnings
 from sqlalchemy.testing.util import random_choices
 from sqlalchemy.types import ARRAY
 from sqlalchemy.types import JSON
@@ -144,6 +146,25 @@ dml.Update.argument_for("sqlite", "foo", None)
 dml.Delete.argument_for("sqlite", "foo", None)
 
 
+class MyType1(TypeDecorator):
+    cache_ok = True
+    impl = String
+
+
+class MyType2(TypeDecorator):
+    cache_ok = True
+    impl = Integer
+
+
+class MyType3(TypeDecorator):
+    impl = Integer
+
+    cache_ok = True
+
+    def __init__(self, arg):
+        self.arg = arg
+
+
 class CoreFixtures(object):
     # lambdas which return a tuple of ColumnElement objects.
     # must return at least two objects that should compare differently.
@@ -684,6 +705,20 @@ class CoreFixtures(object):
         lambda: (table_a, table_b),
     ]
 
+    type_cache_key_fixtures = [
+        lambda: (
+            column("q") == column("x"),
+            column("q") == column("y"),
+            column("z") == column("x"),
+            column("z", String(50)) == column("x", String(50)),
+            column("z", String(50)) == column("x", String(30)),
+            column("z", String(50)) == column("x", Integer),
+            column("z", MyType1()) == column("x", MyType2()),
+            column("z", MyType1()) == column("x", MyType3("x")),
+            column("z", MyType1()) == column("x", MyType3("y")),
+        )
+    ]
+
     dont_compare_values_fixtures = [
         lambda: (
             # note the in_(...) all have different column names because
@@ -1126,6 +1161,7 @@ class CacheKeyTest(CacheKeyFixture, CoreFixtures, fixtures.TestBase):
         for fixtures_, compare_values in [
             (self.fixtures, True),
             (self.dont_compare_values_fixtures, False),
+            (self.type_cache_key_fixtures, False),
         ]:
             for fixture in fixtures_:
                 self._run_cache_key_fixture(fixture, compare_values)
@@ -1669,3 +1705,78 @@ class ExecutableFlagsTest(fixtures.TestBase):
             is_true(case.is_select)
         else:
             is_false(case.is_select)
+
+
+class TypesTest(fixtures.TestBase):
+    def test_typedec_no_cache(self):
+        class MyType(TypeDecorator):
+            impl = String
+
+        expr = column("q", MyType()) == 1
+
+        with expect_warnings(
+            r"TypeDecorator MyType\(\) will not produce a cache key"
+        ):
+            is_(expr._generate_cache_key(), None)
+
+    def test_typedec_cache_false(self):
+        class MyType(TypeDecorator):
+            impl = String
+
+            cache_ok = False
+
+        expr = column("q", MyType()) == 1
+
+        is_(expr._generate_cache_key(), None)
+
+    def test_typedec_cache_ok(self):
+        class MyType(TypeDecorator):
+            impl = String
+
+            cache_ok = True
+
+        def go1():
+            expr = column("q", MyType()) == 1
+            return expr
+
+        def go2():
+            expr = column("p", MyType()) == 1
+            return expr
+
+        c1 = go1()._generate_cache_key()[0]
+        c2 = go1()._generate_cache_key()[0]
+        c3 = go2()._generate_cache_key()[0]
+
+        eq_(c1, c2)
+        ne_(c1, c3)
+
+    def test_typedec_cache_ok_params(self):
+        class MyType(TypeDecorator):
+            impl = String
+
+            cache_ok = True
+
+            def __init__(self, p1, p2):
+                self.p1 = p1
+                self._p2 = p2
+
+        def go1():
+            expr = column("q", MyType("x", "y")) == 1
+            return expr
+
+        def go2():
+            expr = column("q", MyType("q", "y")) == 1
+            return expr
+
+        def go3():
+            expr = column("q", MyType("x", "z")) == 1
+            return expr
+
+        c1 = go1()._generate_cache_key()[0]
+        c2 = go1()._generate_cache_key()[0]
+        c3 = go2()._generate_cache_key()[0]
+        c4 = go3()._generate_cache_key()[0]
+
+        eq_(c1, c2)
+        ne_(c1, c3)
+        eq_(c1, c4)
index 007dc157b90fc9ea2afd34c17415fd823f419e3e..ef924e06819282b546b7c8bbb39165c8adbfe862 100644 (file)
@@ -407,6 +407,7 @@ class DefaultRoundTripTest(fixtures.TablesTest):
 
         class MyType(TypeDecorator):
             impl = String(50)
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 if value is not None:
@@ -1084,6 +1085,7 @@ class AutoIncrementTest(fixtures.TestBase):
     def test_autoinc_detection_no_affinity(self):
         class MyType(TypeDecorator):
             impl = TypeEngine
+            cache_ok = True
 
         assert MyType()._type_affinity is None
         t = Table("x", MetaData(), Column("id", MyType(), primary_key=True))
@@ -1212,6 +1214,8 @@ class SpecialTypePKTest(fixtures.TestBase):
         class MyInteger(TypeDecorator):
             impl = Integer
 
+            cache_ok = True
+
             def process_bind_param(self, value, dialect):
                 if value is None:
                     return None
index 90da5087543df13cb4890a702db44f6cdc427a05..9e0253052511373ed3fc7020eff209e705f34af3 100644 (file)
@@ -2085,6 +2085,7 @@ class SchemaTypeTest(fixtures.TestBase):
 
     class MyTypeDecAndSchema(TypeDecorator, sqltypes.SchemaType):
         impl = String()
+        cache_ok = True
 
         evt_targets = ()
 
@@ -2114,6 +2115,7 @@ class SchemaTypeTest(fixtures.TestBase):
 
         class MyType(TypeDecorator):
             impl = target_typ
+            cache_ok = True
 
         typ = MyType()
         self._test_before_parent_attach(typ, target_typ)
@@ -2129,6 +2131,7 @@ class SchemaTypeTest(fixtures.TestBase):
     def test_before_parent_attach_typedec_of_schematype(self):
         class MyType(TypeDecorator, sqltypes.SchemaType):
             impl = String
+            cache_ok = True
 
         typ = MyType()
         self._test_before_parent_attach(typ)
@@ -2136,6 +2139,7 @@ class SchemaTypeTest(fixtures.TestBase):
     def test_before_parent_attach_schematype_of_typedec(self):
         class MyType(sqltypes.SchemaType, TypeDecorator):
             impl = String
+            cache_ok = True
 
         typ = MyType()
         self._test_before_parent_attach(typ)
@@ -2243,6 +2247,7 @@ class SchemaTypeTest(fixtures.TestBase):
     def test_to_metadata_copy_decorated(self):
         class MyDecorated(TypeDecorator):
             impl = self.MyType
+            cache_ok = True
 
         m1 = MetaData()
 
index 8fe802bf3db54de853127c6db49ff212f1e2eb79..932d30742bf6a8761f941cc2873b5fc11b825fbc 100644 (file)
@@ -500,6 +500,7 @@ class TypeDecoratorComparatorTest(_CustomComparatorTests, fixtures.TestBase):
     def _add_override_factory(self):
         class MyInteger(TypeDecorator):
             impl = Integer
+            cache_ok = True
 
             class comparator_factory(TypeDecorator.Comparator):
                 def __init__(self, expr):
@@ -520,6 +521,7 @@ class TypeDecoratorTypeDecoratorComparatorTest(
     def _add_override_factory(self):
         class MyIntegerOne(TypeDecorator):
             impl = Integer
+            cache_ok = True
 
             class comparator_factory(TypeDecorator.Comparator):
                 def __init__(self, expr):
@@ -533,6 +535,7 @@ class TypeDecoratorTypeDecoratorComparatorTest(
 
         class MyIntegerTwo(TypeDecorator):
             impl = MyIntegerOne
+            cache_ok = True
 
         return MyIntegerTwo
 
@@ -556,6 +559,7 @@ class TypeDecoratorWVariantComparatorTest(
 
         class MyInteger(TypeDecorator):
             impl = Integer
+            cache_ok = True
 
             class comparator_factory(TypeDecorator.Comparator):
                 def __init__(self, expr):
@@ -587,6 +591,7 @@ class CustomEmbeddedinTypeDecoratorTest(
 
         class MyDecInteger(TypeDecorator):
             impl = MyInteger
+            cache_ok = True
 
         return MyDecInteger
 
index 33245bfbcedba33dfa4c1ff46f75a4bef370dee1..a22cf1098ccc8e391a112c68489dcda8209f5b70 100644 (file)
@@ -353,6 +353,7 @@ class QueryTest(fixtures.TablesTest):
 
         class MyInteger(TypeDecorator):
             impl = Integer
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 return int(value[4:])
@@ -783,6 +784,7 @@ class QueryTest(fixtures.TablesTest):
 
         class NameWithProcess(TypeDecorator):
             impl = String
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 return value[3:]
index 2054b3cf19c592f79aa5e48c1fa9cbd2fe604854..44422257af71d6b0a75442ae3b54cd06e0302f29 100644 (file)
@@ -978,18 +978,21 @@ class CursorResultTest(fixtures.TablesTest):
 
         class Goofy1(TypeDecorator):
             impl = String
+            cache_ok = True
 
             def process_result_value(self, value, dialect):
                 return value + "a"
 
         class Goofy2(TypeDecorator):
             impl = String
+            cache_ok = True
 
             def process_result_value(self, value, dialect):
                 return value + "b"
 
         class Goofy3(TypeDecorator):
             impl = String
+            cache_ok = True
 
             def process_result_value(self, value, dialect):
                 return value + "c"
@@ -2527,6 +2530,7 @@ class AlternateCursorResultTest(fixtures.TablesTest):
     def _test_result_processor(self, cls, use_cache):
         class MyType(TypeDecorator):
             impl = String()
+            cache_ok = True
 
             def process_result_value(self, value, dialect):
                 return "HI " + value
index 62d9ab75ec48690f8cdd5d901ca1702538a70bd1..a0d69e782c16df25d7914eb603cb5ecc83210f84 100644 (file)
@@ -100,6 +100,7 @@ class ReturningTest(fixtures.TablesTest, AssertsExecutionResults):
     def define_tables(cls, metadata):
         class GoofyType(TypeDecorator):
             impl = String
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 if value is None:
@@ -386,6 +387,7 @@ class CompositeStatementTest(fixtures.TestBase):
     def test_select_doesnt_pollute_result(self, connection):
         class MyType(TypeDecorator):
             impl = Integer
+            cache_ok = True
 
             def process_result_value(self, value, dialect):
                 raise Exception("I have not been selected")
index b54ef02fd9d21475ad0d09ec195db1c433657d84..add07e01322c1235e3ae8dbfa9971d187734f4af 100644 (file)
@@ -604,6 +604,7 @@ class SelectableTest(
     def test_type_coerce_preserve_subq(self):
         class MyType(TypeDecorator):
             impl = Integer
+            cache_ok = True
 
         stmt = select(type_coerce(column("x"), MyType).label("foo"))
         subq = stmt.subquery()
index 5f278fb559248211040179ae2d343b6df463fb62..c4ed8121e214b67459ce17d72ae7e0d90e9d7dd1 100644 (file)
@@ -37,6 +37,7 @@ class _ExprFixture(object):
     def _type_decorator_outside_fixture(self):
         class MyString(TypeDecorator):
             impl = String
+            cache_ok = True
 
             def bind_expression(self, bindvalue):
                 return func.outside_bind(bindvalue)
@@ -56,6 +57,7 @@ class _ExprFixture(object):
 
         class MyString(TypeDecorator):
             impl = MyInsideString
+            cache_ok = True
 
         return self._test_table(MyString)
 
@@ -69,6 +71,7 @@ class _ExprFixture(object):
 
         class MyString(TypeDecorator):
             impl = String
+            cache_ok = True
 
             # this works because when the compiler calls dialect_impl(),
             # a copy of MyString is created which has just this impl
@@ -427,6 +430,7 @@ class TypeDecRoundTripTest(fixtures.TablesTest, RoundTripTestBase):
     def define_tables(cls, metadata):
         class MyString(TypeDecorator):
             impl = String
+            cache_ok = True
 
             def bind_expression(self, bindvalue):
                 return func.lower(bindvalue)
index e63197ae2f98c6a41f8c64329da2c264b2cbbafd..9db0fee3b07718095eab1ca014d73c9edb87d0dc 100644 (file)
@@ -375,6 +375,7 @@ class TypeAffinityTest(fixtures.TestBase):
 
         class MyType(TypeDecorator):
             impl = CHAR
+            cache_ok = True
 
             def load_dialect_impl(self, dialect):
                 if dialect.name == "postgresql":
@@ -504,6 +505,7 @@ class _UserDefinedTypeFixture(object):
 
         class MyDecoratedType(types.TypeDecorator):
             impl = String
+            cache_ok = True
 
             def bind_processor(self, dialect):
                 impl_processor = super(MyDecoratedType, self).bind_processor(
@@ -530,6 +532,7 @@ class _UserDefinedTypeFixture(object):
 
         class MyNewUnicodeType(types.TypeDecorator):
             impl = Unicode
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 return "BIND_IN" + value
@@ -542,6 +545,7 @@ class _UserDefinedTypeFixture(object):
 
         class MyNewIntType(types.TypeDecorator):
             impl = Integer
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 return value * 10
@@ -561,6 +565,7 @@ class _UserDefinedTypeFixture(object):
 
         class MyUnicodeType(types.TypeDecorator):
             impl = Unicode
+            cache_ok = True
 
             def bind_processor(self, dialect):
                 impl_processor = super(MyUnicodeType, self).bind_processor(
@@ -587,6 +592,7 @@ class _UserDefinedTypeFixture(object):
 
         class MyDecOfDec(types.TypeDecorator):
             impl = MyNewIntType
+            cache_ok = True
 
         Table(
             "users",
@@ -735,6 +741,7 @@ class UserDefinedTest(
     def test_typedecorator_literal_render(self):
         class MyType(types.TypeDecorator):
             impl = String
+            cache_ok = True
 
             def process_literal_param(self, value, dialect):
                 return "HI->%s<-THERE" % value
@@ -767,6 +774,7 @@ class UserDefinedTest(
         # value rendering.
         class MyType(types.TypeDecorator):
             impl = String
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 return "HI->%s<-THERE" % value
@@ -796,6 +804,7 @@ class UserDefinedTest(
 
                 class MyType(types.TypeDecorator):
                     impl = impl_
+                    cache_ok = True
 
                 dec_type = MyType(**kw)
 
@@ -813,6 +822,7 @@ class UserDefinedTest(
     def test_user_defined_typedec_impl(self):
         class MyType(types.TypeDecorator):
             impl = Float
+            cache_ok = True
 
             def load_dialect_impl(self, dialect):
                 if dialect.name == "sqlite":
@@ -838,6 +848,7 @@ class UserDefinedTest(
     def test_typedecorator_schematype_constraint(self, typ):
         class B(TypeDecorator):
             impl = typ
+            cache_ok = True
 
         t1 = Table("t1", MetaData(), Column("q", B(create_constraint=True)))
         eq_(
@@ -849,6 +860,8 @@ class UserDefinedTest(
         class MyType(TypeDecorator):
             impl = VARCHAR
 
+            cache_ok = True
+
         eq_(repr(MyType(45)), "MyType(length=45)")
 
     def test_user_defined_typedec_impl_bind(self):
@@ -868,6 +881,7 @@ class UserDefinedTest(
 
         class MyType(types.TypeDecorator):
             impl = TypeOne
+            cache_ok = True
 
             def load_dialect_impl(self, dialect):
                 if dialect.name == "sqlite":
@@ -951,6 +965,7 @@ class TypeCoerceCastTest(fixtures.TablesTest):
     def define_tables(cls, metadata):
         class MyType(types.TypeDecorator):
             impl = String(50)
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 return "BIND_IN" + str(value)
@@ -1252,6 +1267,7 @@ class VariantBackendTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_type_decorator_variant_one_roundtrip(self, variant_roundtrip):
         class Foo(TypeDecorator):
             impl = String(50)
+            cache_ok = True
 
         if testing.against("postgresql"):
             data = [5, 6, 10]
@@ -1288,6 +1304,7 @@ class VariantBackendTest(fixtures.TestBase, AssertsCompiledSQL):
 
         class Foo(TypeDecorator):
             impl = variant
+            cache_ok = True
 
         if testing.against("postgresql"):
             data = assert_data = [5, 6, 10]
@@ -1305,6 +1322,7 @@ class VariantBackendTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_type_decorator_variant_three(self, variant_roundtrip):
         class Foo(TypeDecorator):
             impl = String
+            cache_ok = True
 
         if testing.against("postgresql"):
             data = ["five", "six", "ten"]
@@ -1318,6 +1336,7 @@ class VariantBackendTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_type_decorator_compile_variant_one(self):
         class Foo(TypeDecorator):
             impl = String
+            cache_ok = True
 
         self.assert_compile(
             Foo().with_variant(Integer, "sqlite"),
@@ -1356,6 +1375,7 @@ class VariantBackendTest(fixtures.TestBase, AssertsCompiledSQL):
 
         class Foo(TypeDecorator):
             impl = variant
+            cache_ok = True
 
         self.assert_compile(
             Foo().with_variant(Integer, "sqlite"),
@@ -1372,6 +1392,7 @@ class VariantBackendTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_type_decorator_compile_variant_three(self):
         class Foo(TypeDecorator):
             impl = String
+            cache_ok = True
 
         self.assert_compile(
             Integer().with_variant(Foo(), "postgresql"),
@@ -2293,6 +2314,8 @@ class EnumTest(AssertsCompiledSQL, fixtures.TablesTest):
                 self.name = name
 
         class MyEnum(TypeDecorator):
+            cache_ok = True
+
             def __init__(self, values):
                 self.impl = Enum(
                     *[v.name for v in values],
@@ -2431,6 +2454,7 @@ class BinaryTest(fixtures.TablesTest, AssertsExecutionResults):
 
         class MyPickleType(types.TypeDecorator):
             impl = PickleType
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 if value:
@@ -2788,6 +2812,8 @@ class ExpressionTest(
         class MyTypeDec(types.TypeDecorator):
             impl = String
 
+            cache_ok = True
+
             def process_bind_param(self, value, dialect):
                 return "BIND_IN" + str(value)
 
@@ -2797,6 +2823,8 @@ class ExpressionTest(
         class MyDecOfDec(types.TypeDecorator):
             impl = MyTypeDec
 
+            cache_ok = True
+
         Table(
             "test",
             metadata,
@@ -2991,14 +3019,17 @@ class ExpressionTest(
         class CoerceNothing(TypeDecorator):
             coerce_to_is_types = ()
             impl = Integer
+            cache_ok = True
 
         class CoerceBool(TypeDecorator):
             coerce_to_is_types = (bool,)
             impl = Boolean
+            cache_ok = True
 
         class CoerceNone(TypeDecorator):
             coerce_to_is_types = (type(None),)
             impl = Integer
+            cache_ok = True
 
         c1 = column("x", CoerceNothing())
         c2 = column("x", CoerceBool())
@@ -3027,6 +3058,7 @@ class ExpressionTest(
     def test_typedec_righthand_coercion(self, connection):
         class MyTypeDec(types.TypeDecorator):
             impl = String
+            cache_ok = True
 
             def process_bind_param(self, value, dialect):
                 return "BIND_IN" + str(value)
@@ -3531,6 +3563,7 @@ class BooleanTest(
 
         class MyBool(TypeDecorator):
             impl = Boolean(create_constraint=True)
+            cache_ok = True
 
             # future method
             def process_literal_param(self, value, dialect):