diff --git a/doc/build/changelog/unreleased_14/6436.rst b/doc/build/changelog/unreleased_14/6436.rst new file mode 100644 index 0000000000..e6b83d852e --- /dev/null +++ b/doc/build/changelog/unreleased_14/6436.rst @@ -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. diff --git a/doc/build/core/custom_types.rst b/doc/build/core/custom_types.rst index 31c66b1e6d..1f506f168d 100644 --- a/doc/build/core/custom_types.rst +++ b/doc/build/core/custom_types.rst @@ -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): diff --git a/lib/sqlalchemy/dialects/mssql/information_schema.py b/lib/sqlalchemy/dialects/mssql/information_schema.py index c379207974..eb8f6db5b6 100644 --- a/lib/sqlalchemy/dialects/mssql/information_schema.py +++ b/lib/sqlalchemy/dialects/mssql/information_schema.py @@ -25,6 +25,7 @@ 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) diff --git a/lib/sqlalchemy/dialects/sqlite/pysqlite.py b/lib/sqlalchemy/dialects/sqlite/pysqlite.py index 20a4bb7acb..8df0037bf2 100644 --- a/lib/sqlalchemy/dialects/sqlite/pysqlite.py +++ b/lib/sqlalchemy/dialects/sqlite/pysqlite.py @@ -312,6 +312,7 @@ def regexp(a, b): class MixedBinary(TypeDecorator): impl = String + cache_ok = True def process_result_value(self, value, dialect): if isinstance(value, str): diff --git a/lib/sqlalchemy/sql/sqltypes.py b/lib/sqlalchemy/sql/sqltypes.py index 024b9f01e7..4f8654afd9 100644 --- a/lib/sqlalchemy/sql/sqltypes.py +++ b/lib/sqlalchemy/sql/sqltypes.py @@ -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. diff --git a/lib/sqlalchemy/sql/traversals.py b/lib/sqlalchemy/sql/traversals.py index f2099f1911..e8a8052858 100644 --- a/lib/sqlalchemy/sql/traversals.py +++ b/lib/sqlalchemy/sql/traversals.py @@ -152,7 +152,11 @@ def _gen_cache_key(self, anon_map, bindparams): # 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): diff --git a/lib/sqlalchemy/sql/type_api.py b/lib/sqlalchemy/sql/type_api.py index 69cd3c5caf..47f6c30052 100644 --- a/lib/sqlalchemy/sql/type_api.py +++ b/lib/sqlalchemy/sql/type_api.py @@ -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 MyType(types.TypeDecorator): impl = types.Unicode + cache_ok = True + def process_bind_param(self, value, dialect): return "PREFIX:" + value @@ -887,6 +890,16 @@ def copy(self, **kw): 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 @@ def coerce_compared_value(self, op, value): 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 @@ def __init__(self, *args, **kwargs): """ + 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:: + + (, ('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 @@ def comparator_factory(self): {}, ) + @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`. diff --git a/lib/sqlalchemy/testing/suite/test_types.py b/lib/sqlalchemy/testing/suite/test_types.py index ebcceaae7c..3e54d87a44 100644 --- a/lib/sqlalchemy/testing/suite/test_types.py +++ b/lib/sqlalchemy/testing/suite/test_types.py @@ -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 diff --git a/test/dialect/mssql/test_types.py b/test/dialect/mssql/test_types.py index 798dbfc1fd..269638cd35 100644 --- a/test/dialect/mssql/test_types.py +++ b/test/dialect/mssql/test_types.py @@ -1196,6 +1196,7 @@ def test_text_text_literal_binds(self): class MyPickleType(types.TypeDecorator): impl = PickleType + cache_ok = True def process_bind_param(self, value, dialect): if value: diff --git a/test/dialect/mysql/test_compiler.py b/test/dialect/mysql/test_compiler.py index 84646d3802..8d311fb6c8 100644 --- a/test/dialect/mysql/test_compiler.py +++ b/test/dialect/mysql/test_compiler.py @@ -693,6 +693,7 @@ def test_cast(self, type_, expected): def test_cast_type_decorator(self): class MyInteger(sqltypes.TypeDecorator): impl = Integer + cache_ok = True type_ = MyInteger() t = sql.table("t", sql.column("col")) diff --git a/test/dialect/mysql/test_types.py b/test/dialect/mysql/test_types.py index 355d774ac0..cf39ce4bc0 100644 --- a/test/dialect/mysql/test_types.py +++ b/test/dialect/mysql/test_types.py @@ -653,6 +653,7 @@ def test_boolean_roundtrip_reflected( class MyTime(TypeDecorator): impl = TIMESTAMP + cache_ok = True @testing.combinations( (TIMESTAMP,), diff --git a/test/dialect/oracle/test_compiler.py b/test/dialect/oracle/test_compiler.py index e198fa48a9..2bbc14e99a 100644 --- a/test/dialect/oracle/test_compiler.py +++ b/test/dialect/oracle/test_compiler.py @@ -568,6 +568,7 @@ def test_for_update_of_w_limit_offset_adaption_partial_col_unpresent(self): 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() diff --git a/test/dialect/oracle/test_types.py b/test/dialect/oracle/test_types.py index f3f8ef7c2d..4b11960257 100644 --- a/test/dialect/oracle/test_types.py +++ b/test/dialect/oracle/test_types.py @@ -1189,6 +1189,7 @@ def test_setinputsizes( class TestTypeDec(TypeDecorator): impl = NullType() + cache_ok = True def load_dialect_impl(self, dialect): if dialect.name == "oracle": diff --git a/test/dialect/postgresql/test_dialect.py b/test/dialect/postgresql/test_dialect.py index 1bd947f6a3..1382788224 100644 --- a/test/dialect/postgresql/test_dialect.py +++ b/test/dialect/postgresql/test_dialect.py @@ -1226,6 +1226,8 @@ def test_serial_integer(self): class BITD(TypeDecorator): impl = Integer + cache_ok = True + def load_dialect_impl(self, dialect): if dialect.name == "postgresql": return BigInteger() diff --git a/test/dialect/postgresql/test_on_conflict.py b/test/dialect/postgresql/test_on_conflict.py index 489084de79..dcf112de66 100644 --- a/test/dialect/postgresql/test_on_conflict.py +++ b/test/dialect/postgresql/test_on_conflict.py @@ -42,6 +42,8 @@ def define_tables(cls, metadata): class SpecialType(sqltypes.TypeDecorator): impl = String + cache_ok = True + def process_bind_param(self, value, dialect): return value + " processed" diff --git a/test/dialect/postgresql/test_types.py b/test/dialect/postgresql/test_types.py index 2f975e4a8b..da550bcb9c 100644 --- a/test/dialect/postgresql/test_types.py +++ b/test/dialect/postgresql/test_types.py @@ -746,6 +746,7 @@ def test_schema_reflection(self, metadata, connection): 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 @@ def test_array_overlap_exec(self, connection): 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) diff --git a/test/dialect/test_sqlite.py b/test/dialect/test_sqlite.py index bae97bb9a2..95dbecdc8c 100644 --- a/test/dialect/test_sqlite.py +++ b/test/dialect/test_sqlite.py @@ -1682,6 +1682,7 @@ def test_sqlite_no_autoincrement(self): def test_sqlite_autoincrement_int_affinity(self): class MyInteger(sqltypes.TypeDecorator): impl = Integer + cache_ok = True table = Table( "autoinctable", @@ -2693,6 +2694,7 @@ def define_tables(cls, metadata): class SpecialType(sqltypes.TypeDecorator): impl = String + cache_ok = True def process_bind_param(self, value, dialect): return value + " processed" diff --git a/test/engine/test_execute.py b/test/engine/test_execute.py index f4449383a9..9ee8d2480c 100644 --- a/test/engine/test_execute.py +++ b/test/engine/test_execute.py @@ -336,6 +336,7 @@ class NonStandardException(OperationalError): 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 MyException(Exception, tsa.exc.DontWrapMixin): class MyType(TypeDecorator): impl = Integer + cache_ok = True def process_bind_param(self, value, dialect): raise MyException("nope") @@ -2575,6 +2577,7 @@ def test_exception_event_ad_hoc_context(self): class MyType(TypeDecorator): impl = Integer + cache_ok = True def process_bind_param(self, value, dialect): raise nope diff --git a/test/ext/mypy/files/type_decorator.py b/test/ext/mypy/files/type_decorator.py index 83f603c63e..07a13caee4 100644 --- a/test/ext/mypy/files/type_decorator.py +++ b/test/ext/mypy/files/type_decorator.py @@ -12,6 +12,7 @@ class IntToStr(TypeDecorator[int]): impl = String + cache_ok = True def process_bind_param( self, diff --git a/test/ext/test_mutable.py b/test/ext/test_mutable.py index e89107c6d3..49d3b9d90a 100644 --- a/test/ext/test_mutable.py +++ b/test/ext/test_mutable.py @@ -970,6 +970,7 @@ def define_tables(cls, metadata): 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 @@ def define_tables(cls, metadata): 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 @@ def define_tables(cls, metadata): 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 @@ def define_tables(cls, metadata): class JSONEncodedDict(TypeDecorator): impl = VARCHAR(50) + cache_ok = True def process_bind_param(self, value, dialect): if value is not None: diff --git a/test/orm/test_lazy_relations.py b/test/orm/test_lazy_relations.py index a0b92a28a7..d75b46886c 100644 --- a/test/orm/test_lazy_relations.py +++ b/test/orm/test_lazy_relations.py @@ -835,9 +835,11 @@ def test_uses_get_compatible_types(self): 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 diff --git a/test/orm/test_naturalpks.py b/test/orm/test_naturalpks.py index 6ad612b740..4c2e84208d 100644 --- a/test/orm/test_naturalpks.py +++ b/test/orm/test_naturalpks.py @@ -1790,6 +1790,7 @@ def __hash__(self): 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"]) diff --git a/test/orm/test_query.py b/test/orm/test_query.py index d26f94bb88..7f2a0f7154 100644 --- a/test/orm/test_query.py +++ b/test/orm/test_query.py @@ -531,6 +531,7 @@ def test_unhashable_type(self): class MyType(TypeDecorator): impl = Integer hashable = False + cache_ok = True def process_result_value(self, value, dialect): return [value] diff --git a/test/orm/test_relationships.py b/test/orm/test_relationships.py index a065b4046a..867994866c 100644 --- a/test/orm/test_relationships.py +++ b/test/orm/test_relationships.py @@ -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 diff --git a/test/orm/test_unitofworkv2.py b/test/orm/test_unitofworkv2.py index 06ec19e52b..365a9b7601 100644 --- a/test/orm/test_unitofworkv2.py +++ b/test/orm/test_unitofworkv2.py @@ -2803,6 +2803,7 @@ def __eq__(self, other): 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 @@ def define_tables(cls, metadata): class EvalsNull(TypeDecorator): impl = String(50) + cache_ok = True + cache_ok = True should_evaluate_none = True diff --git a/test/orm/test_versioning.py b/test/orm/test_versioning.py index c38434c9c1..54500e599d 100644 --- a/test/orm/test_versioning.py +++ b/test/orm/test_versioning.py @@ -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) diff --git a/test/sql/test_compare.py b/test/sql/test_compare.py index 21a349d76f..257776c506 100644 --- a/test/sql/test_compare.py +++ b/test/sql/test_compare.py @@ -23,6 +23,7 @@ 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_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 @@ class Foo: 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 @@ def test_cache_key(self): 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 @@ def test_is_select(self, case): 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) diff --git a/test/sql/test_defaults.py b/test/sql/test_defaults.py index 007dc157b9..ef924e0681 100644 --- a/test/sql/test_defaults.py +++ b/test/sql/test_defaults.py @@ -407,6 +407,7 @@ def gen_default(cls, ctx): 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 @@ def test_autoincrement_single_col(self, metadata, connection): 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 @@ def setup_test_class(cls): class MyInteger(TypeDecorator): impl = Integer + cache_ok = True + def process_bind_param(self, value, dialect): if value is None: return None diff --git a/test/sql/test_metadata.py b/test/sql/test_metadata.py index 90da508754..9e02530525 100644 --- a/test/sql/test_metadata.py +++ b/test/sql/test_metadata.py @@ -2085,6 +2085,7 @@ class MyTypeImpl(MyTypeWImpl): class MyTypeDecAndSchema(TypeDecorator, sqltypes.SchemaType): impl = String() + cache_ok = True evt_targets = () @@ -2114,6 +2115,7 @@ class MySchemaType(sqltypes.TypeEngine, sqltypes.SchemaType): class MyType(TypeDecorator): impl = target_typ + cache_ok = True typ = MyType() self._test_before_parent_attach(typ, target_typ) @@ -2129,6 +2131,7 @@ def test_before_parent_attach_array_enclosing_schematype(self): 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 MyType(TypeDecorator, sqltypes.SchemaType): 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 @@ def test_to_metadata_copy_type(self): def test_to_metadata_copy_decorated(self): class MyDecorated(TypeDecorator): impl = self.MyType + cache_ok = True m1 = MetaData() diff --git a/test/sql/test_operators.py b/test/sql/test_operators.py index 8fe802bf3d..932d30742b 100644 --- a/test/sql/test_operators.py +++ b/test/sql/test_operators.py @@ -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 @@ def __and__(self, other): class MyIntegerTwo(TypeDecorator): impl = MyIntegerOne + cache_ok = True return MyIntegerTwo @@ -556,6 +559,7 @@ def __and__(self, other): class MyInteger(TypeDecorator): impl = Integer + cache_ok = True class comparator_factory(TypeDecorator.Comparator): def __init__(self, expr): @@ -587,6 +591,7 @@ def __and__(self, other): class MyDecInteger(TypeDecorator): impl = MyInteger + cache_ok = True return MyDecInteger diff --git a/test/sql/test_query.py b/test/sql/test_query.py index 33245bfbce..a22cf1098c 100644 --- a/test/sql/test_query.py +++ b/test/sql/test_query.py @@ -353,6 +353,7 @@ def test_select_from_bindparam(self, connection): class MyInteger(TypeDecorator): impl = Integer + cache_ok = True def process_bind_param(self, value, dialect): return int(value[4:]) @@ -783,6 +784,7 @@ def test_expanding_in_dont_alter_compiled(self, connection): class NameWithProcess(TypeDecorator): impl = String + cache_ok = True def process_bind_param(self, value, dialect): return value[3:] diff --git a/test/sql/test_resultset.py b/test/sql/test_resultset.py index 2054b3cf19..44422257af 100644 --- a/test/sql/test_resultset.py +++ b/test/sql/test_resultset.py @@ -978,18 +978,21 @@ def test_fetch_unordered_result_map(self, connection): 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 @@ def test_resultprocessor_fully_buffered_cached(self): 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 diff --git a/test/sql/test_returning.py b/test/sql/test_returning.py index 62d9ab75ec..a0d69e782c 100644 --- a/test/sql/test_returning.py +++ b/test/sql/test_returning.py @@ -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") diff --git a/test/sql/test_selectable.py b/test/sql/test_selectable.py index b54ef02fd9..add07e0132 100644 --- a/test/sql/test_selectable.py +++ b/test/sql/test_selectable.py @@ -604,6 +604,7 @@ def test_scalar_subquery_from_subq_same_source(self): 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() diff --git a/test/sql/test_type_expressions.py b/test/sql/test_type_expressions.py index 5f278fb559..c4ed8121e2 100644 --- a/test/sql/test_type_expressions.py +++ b/test/sql/test_type_expressions.py @@ -37,6 +37,7 @@ def column_expression(self, col): 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 @@ def column_expression(self, col): class MyString(TypeDecorator): impl = MyInsideString + cache_ok = True return self._test_table(MyString) @@ -69,6 +71,7 @@ def column_expression(self, col): 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) diff --git a/test/sql/test_types.py b/test/sql/test_types.py index e63197ae2f..9db0fee3b0 100644 --- a/test/sql/test_types.py +++ b/test/sql/test_types.py @@ -375,6 +375,7 @@ def test_decorator_doesnt_cache(self): class MyType(TypeDecorator): impl = CHAR + cache_ok = True def load_dialect_impl(self, dialect): if dialect.name == "postgresql": @@ -504,6 +505,7 @@ def adapt(self, typeobj): 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 @@ def copy(self): class MyNewUnicodeType(types.TypeDecorator): impl = Unicode + cache_ok = True def process_bind_param(self, value, dialect): return "BIND_IN" + value @@ -542,6 +545,7 @@ def copy(self): class MyNewIntType(types.TypeDecorator): impl = Integer + cache_ok = True def process_bind_param(self, value, dialect): return value * 10 @@ -561,6 +565,7 @@ def copy(self): 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 @@ def copy(self): 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 @@ def test_typedecorator_literal_render_fallback_bound(self): # 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 @@ def test_typedecorator_impl(self): class MyType(types.TypeDecorator): impl = impl_ + cache_ok = True dec_type = MyType(**kw) @@ -813,6 +822,7 @@ class MyType(types.TypeDecorator): 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 @@ def load_dialect_impl(self, dialect): 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 @@ def test_type_decorator_repr(self): 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 @@ def go(value): 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 @@ def run(datatype, data, assert_data): 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 @@ def process(value): class Foo(TypeDecorator): impl = variant + cache_ok = True if testing.against("postgresql"): data = assert_data = [5, 6, 10] @@ -1305,6 +1322,7 @@ class Foo(TypeDecorator): 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 Foo(TypeDecorator): 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 @@ def process(value): class Foo(TypeDecorator): impl = variant + cache_ok = True self.assert_compile( Foo().with_variant(Integer, "sqlite"), @@ -1372,6 +1392,7 @@ class Foo(TypeDecorator): 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 @@ def __init__(self, name): 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 @@ def define_tables(cls, metadata): class MyPickleType(types.TypeDecorator): impl = PickleType + cache_ok = True def process_bind_param(self, value, dialect): if value: @@ -2788,6 +2812,8 @@ def adapt_operator(self, op): 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 @@ def process_result_value(self, value, dialect): class MyDecOfDec(types.TypeDecorator): impl = MyTypeDec + cache_ok = True + Table( "test", metadata, @@ -2991,14 +3019,17 @@ def test_typedec_is_adapt(self): 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 CoerceNone(TypeDecorator): 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 @@ def __init__(self, value): class MyBool(TypeDecorator): impl = Boolean(create_constraint=True) + cache_ok = True # future method def process_literal_param(self, value, dialect):