]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Warn when caching is disabled / document
authorMike Bayer <mike_mp@zzzcomputing.com>
Fri, 3 Dec 2021 19:04:05 +0000 (14:04 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Mon, 6 Dec 2021 23:28:50 +0000 (18:28 -0500)
This patch adds new warnings for all elements that
don't indicate their caching behavior, including user-defined
ClauseElement subclasses and third party dialects.
it additionally adds new documentation to discuss an apparent
performance degradation in 1.4 when caching is disabled as a
result in the significant expense incurred by ORM
lazy loaders, which in 1.3 used BakedQuery so were actually
cached.

As a result of adding the warnings, a fair degree of
lesser used SQL expression objects identified that they did not
define caching behavior so would have been producing
``[no key]``, including PostgreSQL constructs ``hstore``
and ``array``.  These have been amended to use inherit
cache where appropriate.  "on conflict" constructs in
PostgreSQL, MySQL, SQLite still explicitly don't generate
a cache key at this time.

The change also adds a test for all constructs via
assert_compile() to assert they will not generate cache
warnings.

Fixes: #7394
Change-Id: I85958affbb99bfad0f5efa21bc8f2a95e7e46981
(cherry picked from commit 22deafe15289d2be55682e1632016004b02b62c0)

45 files changed:
doc/build/changelog/unreleased_14/7394.rst [new file with mode: 0644]
doc/build/core/connections.rst
doc/build/core/expression_api.rst
doc/build/core/foundation.rst [new file with mode: 0644]
doc/build/core/sqlelement.rst
doc/build/core/visitors.rst
doc/build/errors.rst
doc/build/faq/performance.rst
examples/dogpile_caching/caching_query.py
lib/sqlalchemy/dialects/mssql/base.py
lib/sqlalchemy/dialects/mysql/dml.py
lib/sqlalchemy/dialects/postgresql/array.py
lib/sqlalchemy/dialects/postgresql/dml.py
lib/sqlalchemy/dialects/postgresql/ext.py
lib/sqlalchemy/dialects/postgresql/hstore.py
lib/sqlalchemy/dialects/sqlite/dml.py
lib/sqlalchemy/engine/default.py
lib/sqlalchemy/ext/compiler.py
lib/sqlalchemy/orm/attributes.py
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/sql/base.py
lib/sqlalchemy/sql/coercions.py
lib/sqlalchemy/sql/ddl.py
lib/sqlalchemy/sql/elements.py
lib/sqlalchemy/sql/functions.py
lib/sqlalchemy/sql/roles.py
lib/sqlalchemy/sql/traversals.py
lib/sqlalchemy/sql/type_api.py
lib/sqlalchemy/testing/assertions.py
test/dialect/mssql/test_compiler.py
test/engine/test_execute.py
test/ext/test_baked.py
test/ext/test_compiler.py
test/orm/inheritance/test_assorted_poly.py
test/orm/test_cache_key.py
test/orm/test_lambdas.py
test/orm/test_query.py
test/sql/test_compare.py
test/sql/test_functions.py
test/sql/test_labels.py
test/sql/test_lambdas.py
test/sql/test_operators.py
test/sql/test_resultset.py
test/sql/test_types.py

diff --git a/doc/build/changelog/unreleased_14/7394.rst b/doc/build/changelog/unreleased_14/7394.rst
new file mode 100644 (file)
index 0000000..66bda3e
--- /dev/null
@@ -0,0 +1,49 @@
+.. change::
+    :tags: bug, sql
+    :tickets: 7394
+
+    Custom SQL elements, third party dialects, custom or third party datatypes
+    will all generate consistent warnings when they do not clearly opt in or
+    out of SQL statement caching, which is achieved by setting the appropriate
+    attributes on each type of class. The warning links to documentation
+    sections which indicate the appropriate approach for each type of object in
+    order for caching to be enabled.
+
+.. change::
+    :tags: bug, sql
+    :tickets: 7394
+
+    Fixed missing caching directives for a few lesser used classes in SQL Core
+    which would cause ``[no key]`` to be logged for elements which made use of
+    these.
+
+.. change::
+    :tags: bug, postgresql
+    :tickets: 7394
+
+    Fixed missing caching directives for :class:`_postgresql.hstore` and
+    :class:`_postgresql.array` constructs which would cause ``[no key]``
+    to be logged for these elements.
+
+.. change::
+    :tags: bug, orm
+    :tickets: 7394
+
+    User defined ORM options, such as those illustrated in the dogpile.caching
+    example which subclass :class:`_orm.UserDefinedOption`, by definition are
+    handled on every statement execution and do not need to be considered as
+    part of the cache key for the statement. Caching of the base
+    :class:`.ExecutableOption` class has been modified so that it is no longer
+    a :class:`.HasCacheKey` subclass directly, so that the presence of user
+    defined option objects will not have the unwanted side effect of disabling
+    statement caching. Only ORM specific loader and criteria options, which are
+    all internal to SQLAlchemy, now participate within the caching system.
+
+.. change::
+    :tags: bug, orm
+    :tickets: 7394
+
+    Fixed issue where mappings that made use of :func:`_orm.synonym` and
+    potentially other kinds of "proxy" attributes would not in all cases
+    successfully generate a cache key for their SQL statements, leading to
+    degraded performance for those statements.
\ No newline at end of file
index 28d332203b686ae023dc7f7350c7e7cf06bce564..c0efba0f5c048097fde6302706f6515052fbe73f 100644 (file)
@@ -1026,6 +1026,8 @@ what the cache is doing, engine logging will include details about the
 cache's behavior, described in the next section.
 
 
+.. _sql_caching_logging:
+
 Estimating Cache Performance Using Logging
 ------------------------------------------
 
@@ -1293,28 +1295,35 @@ The cache can also be disabled with this argument by sending a value of
 Caching for Third Party Dialects
 ---------------------------------
 
-The caching feature requires that the dialect's compiler produces a SQL
-construct that is generically reusable given a particular cache key.  This means
+The caching feature requires that the dialect's compiler produces SQL
+strings that are safe to reuse for many statement invocations, given
+a particular cache key that is keyed to that SQL string.  This means
 that any literal values in a statement, such as the LIMIT/OFFSET values for
 a SELECT, can not be hardcoded in the dialect's compilation scheme, as
 the compiled string will not be re-usable.   SQLAlchemy supports rendered
 bound parameters using the :meth:`_sql.BindParameter.render_literal_execute`
 method which can be applied to the existing ``Select._limit_clause`` and
-``Select._offset_clause`` attributes by a custom compiler.
-
-As there are many third party dialects, many of which may be generating
-literal values from SQL statements without the benefit of the newer "literal execute"
-feature, SQLAlchemy as of version 1.4.5 has added a flag to dialects known as
-:attr:`_engine.Dialect.supports_statement_cache`.  This flag is tested to be present
-directly on a dialect class, and not any superclasses, so that even a third
-party dialect that subclasses an existing cacheable SQLAlchemy dialect such
-as ``sqlalchemy.dialects.postgresql.PGDialect`` must still specify this flag,
+``Select._offset_clause`` attributes by a custom compiler, which
+are illustrated later in this section.
+
+As there are many third party dialects, many of which may be generating literal
+values from SQL statements without the benefit of the newer "literal execute"
+feature, SQLAlchemy as of version 1.4.5 has added an attribute to dialects
+known as :attr:`_engine.Dialect.supports_statement_cache`. This attribute is
+checked at runtime for its presence directly on a particular dialect's class,
+even if it's already present on a superclass, so that even a third party
+dialect that subclasses an existing cacheable SQLAlchemy dialect such as
+``sqlalchemy.dialects.postgresql.PGDialect`` must still explicitly include this
+attribute for caching to be enabled. The attribute should **only** be enabled
 once the dialect has been altered as needed and tested for reusability of
 compiled SQL statements with differing parameters.
 
-For all third party dialects that don't support this flag, the logging for
-such a dialect will indicate ``dialect does not support caching``.   Dialect
-authors can apply the flag as follows::
+For all third party dialects that don't support this attribute, the logging for
+such a dialect will indicate ``dialect does not support caching``.
+
+When a dialect has been tested against caching, and in particular the SQL
+compiler has been updated to not render any literal LIMIT / OFFSET within
+a SQL string directly, dialect authors can apply the attribute as follows::
 
     from sqlalchemy.engine.default import DefaultDialect
 
@@ -1328,6 +1337,96 @@ The flag needs to be applied to all subclasses of the dialect as well::
 
 .. versionadded:: 1.4.5
 
+    Added the :attr:`.Dialect.supports_statement_cache` attribute.
+
+The typical case for dialect modification follows.
+
+Example: Rendering LIMIT / OFFSET with post compile parameters
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+As an example, suppose a dialect overrides the :meth:`.SQLCompiler.limit_clause`
+method, which produces the "LIMIT / OFFSET" clause for a SQL statement,
+like this::
+
+    # pre 1.4 style code
+    def limit_clause(self, select, **kw):
+        text = ""
+        if select._limit is not None:
+            text += " \n LIMIT %d" % (select._limit, )
+        if select._offset is not None:
+            text += " \n OFFSET %d" % (select._offset, )
+        return text
+
+The above routine renders the :attr:`.Select._limit` and
+:attr:`.Select._offset` integer values as literal integers embedded in the SQL
+statement. This is a common requirement for databases that do not support using
+a bound parameter within the LIMIT/OFFSET clauses of a SELECT statement.
+However, rendering the integer value within the initial compilation stage is
+directly **incompatible** with caching as the limit and offset integer values
+of a :class:`.Select` object are not part of the cache key, so that many
+:class:`.Select` statements with different limit/offset values would not render
+with the correct value.
+
+The correction for the above code is to move the literal integer into
+SQLAlchemy's :ref:`post-compile <change_4808>` facility, which will render the
+literal integer outside of the initial compilation stage, but instead at
+execution time before the statement is sent to the DBAPI.  This is accessed
+within the compilation stage using the :meth:`_sql.BindParameter.render_literal_execute`
+method, in conjunction with using the :attr:`.Select._limit_clause` and
+:attr:`.Select._offset_clause` attributes, which represent the LIMIT/OFFSET
+as a complete SQL expression, as follows::
+
+    # 1.4 cache-compatible code
+    def limit_clause(self, select, **kw):
+        text = ""
+
+        limit_clause = select._limit_clause
+        offset_clause = select._offset_clause
+
+        if select._simple_int_clause(limit_clause):
+            text += " \n LIMIT %s" % (
+                self.process(limit_clause.render_literal_execute(), **kw)
+            )
+        elif limit_clause is not None:
+            # assuming the DB doesn't support SQL expressions for LIMIT.
+            # Otherwise render here normally
+            raise exc.CompileError(
+                "dialect 'mydialect' can only render simple integers for LIMIT"
+            )
+        if select._simple_int_clause(offset_clause):
+            text += " \n OFFSET %s" % (
+                self.process(offset_clause.render_literal_execute(), **kw)
+            )
+        elif offset_clause is not None:
+            # assuming the DB doesn't support SQL expressions for OFFSET.
+            # Otherwise render here normally
+            raise exc.CompileError(
+                "dialect 'mydialect' can only render simple integers for OFFSET"
+            )
+
+        return text
+
+The approach above will generate a compiled SELECT statement that looks like::
+
+    SELECT x FROM y
+    LIMIT __[POSTCOMPILE_param_1]
+    OFFSET __[POSTCOMPILE_param_2]
+
+Where above, the ``__[POSTCOMPILE_param_1]`` and ``__[POSTCOMPILE_param_2]``
+indicators will be populated with their corresponding integer values at
+statement execution time, after the SQL string has been retrieved from the
+cache.
+
+After changes like the above have been made as appropriate, the
+:attr:`.Dialect.supports_statement_cache` flag should be set to ``True``.
+It is strongly recommended that third party dialects make use of the
+`dialect third party test suite <https://github.com/sqlalchemy/sqlalchemy/blob/main/README.dialects.rst>`_
+which will assert that operations like
+SELECTs with LIMIT/OFFSET are correctly rendered and cached.
+
+.. seealso::
+
+    :ref:`faq_new_caching` - in the :ref:`faq_toplevel` section
 
 .. _engine_lambda_caching:
 
index 7d455d2001025bff4c12a1632b281653823f4f1f..236e0e2ee7590d847a7dfa58c765adccd655cc68 100644 (file)
@@ -12,6 +12,7 @@ see :ref:`sqlexpression_toplevel`.
 .. toctree::
     :maxdepth: 3
 
+    foundation
     sqlelement
     operators
     selectable
diff --git a/doc/build/core/foundation.rst b/doc/build/core/foundation.rst
new file mode 100644 (file)
index 0000000..3a017dd
--- /dev/null
@@ -0,0 +1,32 @@
+.. _core_foundation_toplevel:
+
+=================================================
+SQL Expression Language Foundational Constructs
+=================================================
+
+Base classes and mixins that are used to compose SQL Expression Language
+elements.
+
+.. currentmodule:: sqlalchemy.sql.expression
+
+.. autoclass:: CacheKey
+   :members:
+
+.. autoclass:: ClauseElement
+   :members:
+   :inherited-members:
+
+
+.. autoclass:: sqlalchemy.sql.base.DialectKWArgs
+   :members:
+
+
+.. autoclass:: sqlalchemy.sql.traversals.HasCacheKey
+    :members:
+
+.. autoclass:: LambdaElement
+   :members:
+
+.. autoclass:: StatementLambdaElement
+   :members:
+
index 8e65993624d9ac586f7197218d3bd8157634f747..499f26571a8530ede14b4ada5b58459bbcde7e92 100644 (file)
@@ -120,20 +120,12 @@ The classes here are generated using the constructors listed at
 .. autoclass:: BindParameter
    :members:
 
-.. autoclass:: CacheKey
-   :members:
-
 .. autoclass:: Case
    :members:
 
 .. autoclass:: Cast
    :members:
 
-.. autoclass:: ClauseElement
-   :members:
-   :inherited-members:
-
-
 .. autoclass:: ClauseList
    :members:
 
@@ -155,8 +147,6 @@ The classes here are generated using the constructors listed at
    :special-members:
    :inherited-members:
 
-.. autoclass:: sqlalchemy.sql.base.DialectKWArgs
-   :members:
 
 .. autoclass:: Extract
    :members:
@@ -170,9 +160,6 @@ The classes here are generated using the constructors listed at
 .. autoclass:: Label
    :members:
 
-.. autoclass:: LambdaElement
-   :members:
-
 .. autoclass:: Null
    :members:
 
@@ -183,9 +170,6 @@ The classes here are generated using the constructors listed at
 .. autoclass:: Over
    :members:
 
-.. autoclass:: StatementLambdaElement
-   :members:
-
 .. autoclass:: TextClause
    :members:
 
index 6ef466265d457f2ede8ffb6f092f4817f405d3e7..06d839d54cb2c371a835a1eaf60f710159097c83 100644 (file)
@@ -23,4 +23,5 @@ as well as when building out custom SQL expressions using the
 
 .. automodule:: sqlalchemy.sql.visitors
    :members:
-   :private-members:
\ No newline at end of file
+   :private-members:
+
index 2b163ec2692e68ef5681a5470f56a85c807fc0f3..376bfaf4344066f60ff771536b9a635abc9e7858 100644 (file)
@@ -172,6 +172,95 @@ In SQLAlchemy 1.4, this :term:`2.0 style` behavior is enabled when the
 :paramref:`_orm.Session.future` flag is set on :class:`_orm.sessionmaker`
 or :class:`_orm.Session`.
 
+.. _error_cprf:
+.. _caching_caveats:
+
+Object will not produce a cache key, Performance Implications
+--------------------------------------------------------------
+
+SQLAlchemy as of version 1.4 includes a
+:ref:`SQL compilation caching facility <sql_caching>` which will allow
+Core and ORM SQL constructs to cache their stringified form, along with other
+structural information used to fetch results from the statement, allowing the
+relatively expensive string compilation process to be skipped when another
+structurally equivalent construct is next used. This system
+relies upon functionality that is implemented for all SQL constructs, including
+objects such as  :class:`_schema.Column`,
+:func:`_sql.select`, and :class:`_types.TypeEngine` objects, to produce a
+**cache key** which fully represents their state to the degree that it affects
+the SQL compilation process.
+
+If the warnings in question refer to widely used objects such as
+:class:`_schema.Column` objects, and are shown to be affecting the majority of
+SQL constructs being emitted (using the estimation techniques described at
+:ref:`sql_caching_logging`) such that caching is generally not enabled for an
+application, this will negatively impact performance and can in some cases
+effectively produce a **performance degradation** compared to prior SQLAlchemy
+versions. The FAQ at :ref:`faq_new_caching` covers this in additional detail.
+
+Caching disables itself if there's any doubt
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Caching relies on being able to generate a cache key that accurately represents
+the **complete structure** of a statement in a **consistent** fashion. If a particular
+SQL construct (or type) does not have the appropriate directives in place which
+allow it to generate a proper cache key, then caching cannot be safely enabled:
+
+* The cache key must represent the **complete structure**: If the usage of two
+  separate instances of that construct may result in different SQL being
+  rendered, caching the SQL against the first instance of the element using a
+  cache key that does not capture the distinct differences between the first and
+  second elements will result in incorrect SQL being cached and rendered for the
+  second instance.
+
+* The cache key must be **consistent**: If a construct represents state that
+  changes every time, such as a literal value, producing unique SQL for every
+  instance of it, this construct is also not safe to cache, as repeated use of
+  the construct will quickly fill up the statement cache with unique SQL strings
+  that will likely not be used again, defeating the purpose of the cache.
+
+For the above two reasons, SQLAlchemy's caching system is **extremely
+conservative** about deciding to cache the SQL corresponding to an object.
+
+Assertion attributes for caching
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The warning is emitted based on the criteria below.  For further detail on
+each, see the section :ref:`faq_new_caching`.
+
+* The :class:`.Dialect` itself (i.e. the module that is specified by the
+  first part of the URL we pass to :func:`_sa.create_engine`, like
+  ``postgresql+psycopg2://``), must indicate it has been reviewed and tested
+  to support caching correctly, which is indicated by the
+  :attr:`.Dialect.supports_statement_cache` attribute being set to ``True``.
+  When using third party dialects, consult with the maintainers of the dialect
+  so that they may follow the :ref:`steps to ensure caching may be enabled
+  <engine_thirdparty_caching>` in their dialect and publish a new release.
+
+* Third party or user defined types that inherit from either
+  :class:`.TypeDecorator` or :class:`.UserDefinedType` must include the
+  :attr:`.ExternalType.cache_ok` attribute in their definition, including for
+  all derived subclasses, following the guidelines described in the docstring
+  for :attr:`.ExternalType.cache_ok`. As before, if these datatypes are
+  imported from third party libraries, consult with the maintainers of that
+  library so that they may provide the necessary changes to their library and
+  publish a new release.
+
+* Third party or user defined SQL constructs that subclass from classes such
+  as :class:`.ClauseElement`, :class:`_schema.Column`, :class:`_dml.Insert`
+  etc, including simple subclasses as well as those which are designed to
+  work with the :ref:`sqlalchemy.ext.compiler_toplevel`, should normally
+  include the :attr:`.HasCacheKey.inherit_cache` attribute set to ``True``
+  or ``False`` based on the design of the construct, following the guidelines
+  described at :ref:`compilerext_caching`.
+
+.. seealso::
+
+    :ref:`sql_caching_logging` - background on observing cache behavior
+    and efficiency
+
+    :ref:`faq_new_caching` - in the :ref:`faq_toplevel` section
+
 .. _error_s9r1:
 
 Object is being merged into a Session along the backref cascade
index 6e144072131e1cb903bf5479c8d685ec64d6cc72..781d6c79d345e9585b3b2aa45fc05fd597671cd3 100644 (file)
@@ -8,6 +8,166 @@ Performance
     :class: faq
     :backlinks: none
 
+.. _faq_new_caching:
+
+Why is my application slow after upgrading to 1.4 and/or 2.x?
+--------------------------------------------------------------
+
+SQLAlchemy as of version 1.4 includes a
+:ref:`SQL compilation caching facility <sql_caching>` which will allow
+Core and ORM SQL constructs to cache their stringified form, along with other
+structural information used to fetch results from the statement, allowing the
+relatively expensive string compilation process to be skipped when another
+structurally equivalent construct is next used. This system
+relies upon functionality that is implemented for all SQL constructs, including
+objects such as  :class:`_schema.Column`,
+:func:`_sql.select`, and :class:`_types.TypeEngine` objects, to produce a
+**cache key** which fully represents their state to the degree that it affects
+the SQL compilation process.
+
+The caching system allows SQLAlchemy 1.4 and above to be more performant than
+SQLAlchemy 1.3 with regards to the time spent converting SQL constructs into
+strings repeatedly.  However, this only works if caching is enabled for the
+dialect and SQL constructs in use; if not, string compilation is usually
+similar to that of SQLAlchemy 1.3, with a slight decrease in speed in some
+cases.
+
+There is one case however where if SQLAlchemy's new caching system has been
+disabled (for reasons below), performance for the ORM may be in fact
+significantly poorer than that of 1.3 or other prior releases which is due to
+the lack of caching within ORM lazy loaders and object refresh queries, which
+in the 1.3 and earlier releases used the now-legacy ``BakedQuery`` system. If
+an application is seeing significant (30% or higher) degradations in
+performance (measured in time for operations to complete) when switching to
+1.4, this is the likely cause of the issue, with steps to mitigate below.
+
+.. seealso::
+
+    :ref:`sql_caching` - overview of the caching system
+
+    :ref:`caching_caveats` - additional information regarding the warnings
+    generated for elements that don't enable caching.
+
+Step one - turn on SQL logging and confirm whether or not caching is working
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Here, we want to use the technique described at
+:ref:`engine logging <sql_caching_logging>`, looking for statements with the
+``[no key]`` indicator or even ``[dialect does not support caching]``.
+The indicators we would see for SQL statements that are successfully participating
+in the caching system would be indicating ``[generated in Xs]`` when
+statements are invoked for the first time and then
+``[cached since Xs ago]`` for the vast majority of statements subsequent.
+If ``[no key]`` is prevalent in particular for SELECT statements, or
+if caching is disabled entirely due to ``[dialect does not support caching]``,
+this can be the cause of significant performance degradation.
+
+.. seealso::
+
+    :ref:`sql_caching_logging`
+
+
+Step two - identify what constructs are blocking caching from being enabled
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Assuming statements are not being cached, there should be warnings emitted
+early in the application's log (SQLAlchemy 1.4.28 and above only) indicating
+dialects, :class:`.TypeEngine` objects, and SQL constructs that are not
+participating in caching.
+
+For user defined datatypes such as those which extend :class:`_types.TypeDecorator`
+and :class:`_types.UserDefinedType`, the warnings will look like::
+
+    sqlalchemy.ext.SAWarning: MyType will not produce a cache key because the
+    ``cache_ok`` attribute is not set to True. This can have significant
+    performance implications including some performance degradations in
+    comparison to prior SQLAlchemy versions. Set this attribute to True if this
+    type object's state is safe to use in a cache key, or False to disable this
+    warning.
+
+For custom and third party SQL elements, such as those constructed using
+the techniques described at :ref:`sqlalchemy.ext.compiler_toplevel`, these
+warnings will look like::
+
+    sqlalchemy.exc.SAWarning: Class MyClass will not make use of SQL
+    compilation caching as it does not set the 'inherit_cache' attribute to
+    ``True``. This can have significant performance implications including some
+    performance degradations in comparison to prior SQLAlchemy versions. Set
+    this attribute to True if this object can make use of the cache key
+    generated by the superclass. Alternatively, this attribute may be set to
+    False which will disable this warning.
+
+For custom and third party dialects which make use of the :class:`.Dialect`
+class hierarchy, the warnings will look like::
+
+    sqlalchemy.exc.SAWarning: Dialect database:driver will not make use of SQL
+    compilation caching as it does not set the 'supports_statement_cache'
+    attribute to ``True``. This can have significant performance implications
+    including some performance degradations in comparison to prior SQLAlchemy
+    versions. Dialect maintainers should seek to set this attribute to True
+    after appropriate development and testing for SQLAlchemy 1.4 caching
+    support. Alternatively, this attribute may be set to False which will
+    disable this warning.
+
+
+Step three - enable caching for the given objects and/or seek alternatives
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Steps to mitigate the lack of caching include:
+
+* Review and set :attr:`.ExternalType.cache_ok` to ``True`` for all custom types
+  which extend from :class:`_types.TypeDecorator`,
+  :class:`_types.UserDefinedType`, as well as subclasses of these such as
+  :class:`_types.PickleType`.  Set this **only** if the custom type does not
+  include any additional state attributes which affect how it renders SQL::
+
+        class MyCustomType(TypeDecorator):
+            cache_ok = True
+            impl = String
+
+  If the types in use are from a third-party library, consult with the
+  maintainers of that library so that it may be adjusted and released.
+
+  .. seealso::
+
+    :attr:`.ExternalType.cache_ok` - background on requirements to enable
+    caching for custom datatypes.
+
+* Make sure third party dialects set :attr:`.Dialect.supports_statement_cache`
+  to ``True``. What this indicates is that the maintainers of a third party
+  dialect have made sure their dialect works with SQLAlchemy 1.4 or greater,
+  and that their dialect doesn't include any compilation features which may get
+  in the way of caching. As there are some common compilation patterns which
+  can in fact interfere with caching, it's important that dialect maintainers
+  check and test this carefully, adjusting for any of the legacy patterns
+  which won't work with caching.
+
+  .. seealso::
+
+      :ref:`engine_thirdparty_caching` - background and examples for third-party
+      dialects to participate in SQL statement caching.
+
+* Custom SQL classes, including all DQL / DML constructs one might create
+  using the :ref:`sqlalchemy.ext.compiler_toplevel`, as well as ad-hoc
+  subclasses of objects such as :class:`_schema.Column` or
+  :class:`_schema.Table`.   The :attr:`.HasCacheKey.inherit_cache` attribute
+  may be set to ``True`` for trivial subclasses, which do not contain any
+  subclass-specific state information which affects the SQL compilation.
+
+  .. seealso::
+
+    :ref:`compilerext_caching` - guidelines for applying the
+    :attr:`.HasCacheKey.inherit_cache` attribute.
+
+
+.. seealso::
+
+    :ref:`sql_caching` - caching system overview
+
+    :ref:`caching_caveats` - background on warnings emitted when caching
+    is not enabled for specific constructs and/or dialects.
+
+
 .. _faq_how_to_profile:
 
 How can I profile a SQLAlchemy powered application?
index 68f72e5f06e92733f1b757d6de95bff4ee064807..f5065f8df542610b022ce4cf829c3192d875cbaa 100644 (file)
@@ -130,10 +130,19 @@ class FromCache(UserDefinedOption):
         self.expiration_time = expiration_time
         self.ignore_expiration = ignore_expiration
 
+    # this is not needed as of SQLAlchemy 1.4.28;
+    # UserDefinedOption classes no longer participate in the SQL
+    # compilation cache key
     def _gen_cache_key(self, anon_map, bindparams):
         return None
 
     def _generate_cache_key(self, statement, parameters, orm_cache):
+        """generate a cache key with which to key the results of a statement.
+
+        This leverages the use of the SQL compilation cache key which is
+        repurposed as a SQL results key.
+
+        """
         statement_cache_key = statement._generate_cache_key()
 
         key = statement_cache_key.to_offline_string(
index 8d2bc36ee0513384e72b22f7dad56076f333bb3c..d6a35c93768d528e19ea8364f0e8cb44f4ff5095 100644 (file)
@@ -1354,6 +1354,7 @@ class TryCast(sql.elements.Cast):
     __visit_name__ = "try_cast"
 
     stringify_dialect = "mssql"
+    inherit_cache = True
 
     def __init__(self, *arg, **kw):
         """Create a TRY_CAST expression.
index e2f78783c22fa25011472891f5415dc8e86a9a93..790733cbfdab68b44e4cda0ec3e6b492d9cf4166 100644 (file)
@@ -25,6 +25,7 @@ class Insert(StandardInsert):
     """
 
     stringify_dialect = "mysql"
+    inherit_cache = False
 
     @property
     def inserted(self):
index 0cb574dacf7b5f52f34f652c38ff132378ed011b..e57a4fc9accc9cb35b96711a5a832c0572681b0f 100644 (file)
@@ -87,6 +87,7 @@ class array(expression.ClauseList, expression.ColumnElement):
     __visit_name__ = "array"
 
     stringify_dialect = "postgresql"
+    inherit_cache = True
 
     def __init__(self, clauses, **kw):
         clauses = [
index bb6345cf4385eef8847ea6f877a54c842a3be170..4104fe51f7894bd72006f9ab1161c8906a451606 100644 (file)
@@ -35,6 +35,7 @@ class Insert(StandardInsert):
     """
 
     stringify_dialect = "postgresql"
+    inherit_cache = False
 
     @util.memoized_property
     def excluded(self):
index f9e4c1d6cb9527d4a6b05abea9b7b9f28319a4fd..8c3a539be04d96076209f37e36df306666021367 100644 (file)
@@ -54,6 +54,7 @@ class aggregate_order_by(expression.ColumnElement):
     __visit_name__ = "aggregate_order_by"
 
     stringify_dialect = "postgresql"
+    inherit_cache = False
 
     def __init__(self, target, *order_by):
         self.target = coercions.expect(roles.ExpressionElementRole, target)
@@ -99,6 +100,7 @@ class ExcludeConstraint(ColumnCollectionConstraint):
     __visit_name__ = "exclude_constraint"
 
     where = None
+    inherit_cache = False
 
     create_drop_stringify_dialect = "postgresql"
 
index a4090f1ac5934d662091a9e092ff98b8dae388a7..7f42c3ab4e036b227da556c53f82a421ca829913 100644 (file)
@@ -296,41 +296,49 @@ class hstore(sqlfunc.GenericFunction):
 
     type = HSTORE
     name = "hstore"
+    inherit_cache = True
 
 
 class _HStoreDefinedFunction(sqlfunc.GenericFunction):
     type = sqltypes.Boolean
     name = "defined"
+    inherit_cache = True
 
 
 class _HStoreDeleteFunction(sqlfunc.GenericFunction):
     type = HSTORE
     name = "delete"
+    inherit_cache = True
 
 
 class _HStoreSliceFunction(sqlfunc.GenericFunction):
     type = HSTORE
     name = "slice"
+    inherit_cache = True
 
 
 class _HStoreKeysFunction(sqlfunc.GenericFunction):
     type = ARRAY(sqltypes.Text)
     name = "akeys"
+    inherit_cache = True
 
 
 class _HStoreValsFunction(sqlfunc.GenericFunction):
     type = ARRAY(sqltypes.Text)
     name = "avals"
+    inherit_cache = True
 
 
 class _HStoreArrayFunction(sqlfunc.GenericFunction):
     type = ARRAY(sqltypes.Text)
     name = "hstore_to_array"
+    inherit_cache = True
 
 
 class _HStoreMatrixFunction(sqlfunc.GenericFunction):
     type = ARRAY(sqltypes.Text)
     name = "hstore_to_matrix"
+    inherit_cache = True
 
 
 #
index a93e31beba21b688e41c1068ad228c264ac06cec..e4d8bd9434db398ad635becdde0bb6913617e9b3 100644 (file)
@@ -36,6 +36,7 @@ class Insert(StandardInsert):
     """
 
     stringify_dialect = "sqlite"
+    inherit_cache = False
 
     @util.memoized_property
     def excluded(self):
index 1adb88617450cf85509b703a03538d8318f7d1fb..0dac6600ef3fbcc0fd98c14c27c8b991feb64ac1 100644 (file)
@@ -350,10 +350,23 @@ class DefaultDialect(interfaces.Dialect):
 
     @util.memoized_property
     def _supports_statement_cache(self):
-        return (
-            self.__class__.__dict__.get("supports_statement_cache", False)
-            is True
-        )
+        ssc = self.__class__.__dict__.get("supports_statement_cache", None)
+        if ssc is None:
+            util.warn(
+                "Dialect %s:%s will not make use of SQL compilation caching "
+                "as it does not set the 'supports_statement_cache' attribute "
+                "to ``True``.  This can have "
+                "significant performance implications including some "
+                "performance degradations in comparison to prior SQLAlchemy "
+                "versions.  Dialect maintainers should seek to set this "
+                "attribute to True after appropriate development and testing "
+                "for SQLAlchemy 1.4 caching support.   Alternatively, this "
+                "attribute may be set to False which will disable this "
+                "warning." % (self.name, self.driver),
+                code="cprf",
+            )
+
+        return bool(ssc)
 
     @util.memoized_property
     def _type_memos(self):
index c7eb7cc323b45f67201d1eed4266a5759e4883c9..3470407158a8902764ecf3748bb15048ea90ae8e 100644 (file)
@@ -18,7 +18,7 @@ more callables defining its compilation::
     from sqlalchemy.sql.expression import ColumnClause
 
     class MyColumn(ColumnClause):
-        pass
+        inherit_cache = True
 
     @compiles(MyColumn)
     def compile_mycolumn(element, compiler, **kw):
@@ -47,6 +47,7 @@ invoked for the dialect in use::
     from sqlalchemy.schema import DDLElement
 
     class AlterColumn(DDLElement):
+        inherit_cache = False
 
         def __init__(self, column, cmd):
             self.column = column
@@ -64,6 +65,8 @@ invoked for the dialect in use::
 The second ``visit_alter_table`` will be invoked when any ``postgresql``
 dialect is used.
 
+.. _compilerext_compiling_subelements:
+
 Compiling sub-elements of a custom expression construct
 =======================================================
 
@@ -78,6 +81,8 @@ method which can be used for compilation of embedded attributes::
     from sqlalchemy.sql.expression import Executable, ClauseElement
 
     class InsertFromSelect(Executable, ClauseElement):
+        inherit_cache = False
+
         def __init__(self, table, select):
             self.table = table
             self.select = select
@@ -252,6 +257,7 @@ A synopsis is as follows:
 
       class timestamp(ColumnElement):
           type = TIMESTAMP()
+          inherit_cache = True
 
 * :class:`~sqlalchemy.sql.functions.FunctionElement` - This is a hybrid of a
   ``ColumnElement`` and a "from clause" like object, and represents a SQL
@@ -264,6 +270,7 @@ A synopsis is as follows:
 
       class coalesce(FunctionElement):
           name = 'coalesce'
+          inherit_cache = True
 
       @compiles(coalesce)
       def compile(element, compiler, **kw):
@@ -287,6 +294,95 @@ A synopsis is as follows:
   SQL statement that can be passed directly to an ``execute()`` method.  It
   is already implicit within ``DDLElement`` and ``FunctionElement``.
 
+Most of the above constructs also respond to SQL statement caching.   A
+subclassed construct will want to define the caching behavior for the object,
+which usually means setting the flag ``inherit_cache`` to the value of
+``False`` or ``True``.  See the next section :ref:`compilerext_caching`
+for background.
+
+
+.. _compilerext_caching:
+
+Enabling Caching Support for Custom Constructs
+==============================================
+
+SQLAlchemy as of version 1.4 includes a
+:ref:`SQL compilation caching facility <sql_caching>` which will allow
+equivalent SQL constructs to cache their stringified form, along with other
+structural information used to fetch results from the statement.
+
+For reasons discussed at :ref:`caching_caveats`, the implementation of this
+caching system takes a conservative approach towards including custom SQL
+constructs and/or subclasses within the caching system.   This includes that
+any user-defined SQL constructs, including all the examples for this
+extension, will not participate in caching by default unless they positively
+assert that they are able to do so.  The :attr:`.HasCacheKey.inherit_cache`
+attribute when set to ``True`` at the class level of a specific subclass
+will indicate that instances of this class may be safely cached, using the
+cache key generation scheme of the immediate superclass.  This applies
+for example to the "synopsis" example indicated previously::
+
+    class MyColumn(ColumnClause):
+        inherit_cache = True
+
+    @compiles(MyColumn)
+    def compile_mycolumn(element, compiler, **kw):
+        return "[%s]" % element.name
+
+Above, the ``MyColumn`` class does not include any new state that
+affects its SQL compilation; the cache key of ``MyColumn`` instances will
+make use of that of the ``ColumnClause`` superclass, meaning it will take
+into account the class of the object (``MyColumn``), the string name and
+datatype of the object::
+
+    >>> MyColumn("some_name", String())._generate_cache_key()
+    CacheKey(
+        key=('0', <class '__main__.MyColumn'>,
+        'name', 'some_name',
+        'type', (<class 'sqlalchemy.sql.sqltypes.String'>,
+                 ('length', None), ('collation', None))
+    ), bindparams=[])
+
+For objects that are likely to be **used liberally as components within many
+larger statements**, such as :class:`_schema.Column` subclasses and custom SQL
+datatypes, it's important that **caching be enabled as much as possible**, as
+this may otherwise negatively affect performance.
+
+An example of an object that **does** contain state which affects its SQL
+compilation is the one illustrated at :ref:`compilerext_compiling_subelements`;
+this is an "INSERT FROM SELECT" construct that combines together a
+:class:`_schema.Table` as well as a :class:`_sql.Select` construct, each of
+which independently affect the SQL string generation of the construct.  For
+this class, the example illustrates that it simply does not participate in
+caching::
+
+    class InsertFromSelect(Executable, ClauseElement):
+        inherit_cache = False
+
+        def __init__(self, table, select):
+            self.table = table
+            self.select = select
+
+    @compiles(InsertFromSelect)
+    def visit_insert_from_select(element, compiler, **kw):
+        return "INSERT INTO %s (%s)" % (
+            compiler.process(element.table, asfrom=True, **kw),
+            compiler.process(element.select, **kw)
+        )
+
+While it is also possible that the above ``InsertFromSelect`` could be made to
+produce a cache key that is composed of that of the :class:`_schema.Table` and
+:class:`_sql.Select` components together, the API for this is not at the moment
+fully public. However, for an "INSERT FROM SELECT" construct, which is only
+used by itself for specific operations, caching is not as critical as in the
+previous example.
+
+For objects that are **used in relative isolation and are generally
+standalone**, such as custom :term:`DML` constructs like an "INSERT FROM
+SELECT", **caching is generally less critical** as the lack of caching for such
+a construct will have only localized implications for that specific operation.
+
+
 Further Examples
 ================
 
@@ -309,6 +405,7 @@ For PostgreSQL and Microsoft SQL Server::
 
     class utcnow(expression.FunctionElement):
         type = DateTime()
+        inherit_cache = True
 
     @compiles(utcnow, 'postgresql')
     def pg_utcnow(element, compiler, **kw):
@@ -345,6 +442,7 @@ accommodates two arguments::
     class greatest(expression.FunctionElement):
         type = Numeric()
         name = 'greatest'
+        inherit_cache = True
 
     @compiles(greatest)
     def default_greatest(element, compiler, **kw):
@@ -376,7 +474,7 @@ don't have a "false" constant::
     from sqlalchemy.ext.compiler import compiles
 
     class sql_false(expression.ColumnElement):
-        pass
+        inherit_cache = True
 
     @compiles(sql_false)
     def default_false(element, compiler, **kw):
index 513144b8728dfe9db87968cbf08e0a4919efe8e1..9d1e27d977e557456d2944afdc771acfb035f587 100644 (file)
@@ -526,6 +526,11 @@ def create_proxied_attribute(descriptor):
 
         _is_internal_proxy = True
 
+        _cache_key_traversal = [
+            ("key", visitors.ExtendedInternalTraversal.dp_string),
+            ("_parententity", visitors.ExtendedInternalTraversal.dp_multi),
+        ]
+
         @property
         def _impl_uses_objects(self):
             return (
index 9eb362c437b4f8e9c95b9564a21f598fea2c6755..6182588dce605b6b9d9684e995c57bdd307344e2 100644 (file)
@@ -755,15 +755,54 @@ class ORMOption(ExecutableOption):
     _is_strategy_option = False
 
 
-class LoaderOption(ORMOption):
-    """Describe a loader modification to an ORM statement at compilation time.
+class CompileStateOption(HasCacheKey, ORMOption):
+    """base for :class:`.ORMOption` classes that affect the compilation of
+    a SQL query and therefore need to be part of the cache key.
+
+    .. note::  :class:`.CompileStateOption` is generally non-public and
+       should not be used as a base class for user-defined options; instead,
+       use :class:`.UserDefinedOption`, which is easier to use as it does not
+       interact with ORM compilation internals or caching.
+
+    :class:`.CompileStateOption` defines an internal attribute
+    ``_is_compile_state=True`` which has the effect of the ORM compilation
+    routines for SELECT and other statements will call upon these options when
+    a SQL string is being compiled. As such, these classes implement
+    :class:`.HasCacheKey` and need to provide robust ``_cache_key_traversal``
+    structures.
+
+    The :class:`.CompileStateOption` class is used to implement the ORM
+    :class:`.LoaderOption` and :class:`.CriteriaOption` classes.
+
+    .. versionadded:: 1.4.28
 
-    .. versionadded:: 1.4
 
     """
 
     _is_compile_state = True
 
+    def process_compile_state(self, compile_state):
+        """Apply a modification to a given :class:`.CompileState`."""
+
+    def process_compile_state_replaced_entities(
+        self, compile_state, mapper_entities
+    ):
+        """Apply a modification to a given :class:`.CompileState`,
+        given entities that were replaced by with_only_columns() or
+        with_entities().
+
+        .. versionadded:: 1.4.19
+
+        """
+
+
+class LoaderOption(CompileStateOption):
+    """Describe a loader modification to an ORM statement at compilation time.
+
+    .. versionadded:: 1.4
+
+    """
+
     def process_compile_state_replaced_entities(
         self, compile_state, mapper_entities
     ):
@@ -780,7 +819,7 @@ class LoaderOption(ORMOption):
         """Apply a modification to a given :class:`.CompileState`."""
 
 
-class CriteriaOption(ORMOption):
+class CriteriaOption(CompileStateOption):
     """Describe a WHERE criteria modification to an ORM statement at
     compilation time.
 
@@ -788,7 +827,6 @@ class CriteriaOption(ORMOption):
 
     """
 
-    _is_compile_state = True
     _is_criteria_option = True
 
     def process_compile_state(self, compile_state):
index bd897211cad5d8ef9023dadc924fe78aac5aef85..b412caa12f63ba4936dbceafa8bba82102078793 100644 (file)
@@ -3425,6 +3425,8 @@ class AliasOption(interfaces.LoaderOption):
 
         """
 
+    inherit_cache = False
+
     def process_compile_state(self, compile_state):
         pass
 
index aba80222a6b8bbd379360da6a78a77abb96bff05..e04d3b75f0c6d96c587f6423d9845de982ad4ec1 100644 (file)
@@ -768,11 +768,13 @@ class CacheableOptions(Options, HasCacheKey):
         return HasCacheKey._generate_cache_key_for_object(self)
 
 
-class ExecutableOption(HasCopyInternals, HasCacheKey):
+class ExecutableOption(HasCopyInternals):
     _annotations = util.EMPTY_DICT
 
     __visit_name__ = "executable_option"
 
+    _is_has_cache_key = False
+
     def _clone(self, **kw):
         """Create a shallow copy of this ExecutableOption."""
         c = self.__class__.__new__(self.__class__)
@@ -846,7 +848,8 @@ class Executable(roles.StatementRole, Generative):
 
         """
         self._with_options += tuple(
-            coercions.expect(roles.HasCacheKeyRole, opt) for opt in options
+            coercions.expect(roles.ExecutableOptionRole, opt)
+            for opt in options
         )
 
     @_generative
index f888bad4cac77c09958683ec7d1869940368380c..e378d9345f28272caab132d9d0561796d43abd30 100644 (file)
@@ -11,6 +11,7 @@ import re
 from . import operators
 from . import roles
 from . import visitors
+from .base import ExecutableOption
 from .base import Options
 from .traversals import HasCacheKey
 from .visitors import Visitable
@@ -458,6 +459,21 @@ class HasCacheKeyImpl(RoleImpl):
         return element
 
 
+class ExecutableOptionImpl(RoleImpl):
+    __slots__ = ()
+
+    def _implicit_coercions(
+        self, original_element, resolved, argname=None, **kw
+    ):
+        if isinstance(original_element, ExecutableOption):
+            return original_element
+        else:
+            self._raise_for_expected(original_element, argname, resolved)
+
+    def _literal_coercion(self, element, **kw):
+        return element
+
+
 class ExpressionElementImpl(_ColumnCoercions, RoleImpl):
     __slots__ = ()
 
index f8985548ee04896a02c13615e430bb28994d6c6f..b79fee17931d71e7b2344b5b27a5da83d2e441ca 100644 (file)
@@ -22,6 +22,9 @@ from ..util import topological
 
 
 class _DDLCompiles(ClauseElement):
+    _hierarchy_supports_caching = False
+    """disable cache warnings for all _DDLCompiles subclasses. """
+
     def _compiler(self, dialect, **kw):
         """Return a compiler appropriate for this ClauseElement, given a
         Dialect."""
index a276e2cae1e11c787f54555b1b1ca601fc1e063c..08eb37f2ce7b865b0bce7b00a838dd632d9c19fc 100644 (file)
@@ -3654,6 +3654,8 @@ class CollectionAggregate(UnaryExpression):
 
     """
 
+    inherit_cache = True
+
     @classmethod
     def _create_any(cls, expr):
         """Produce an ANY expression.
@@ -3961,7 +3963,7 @@ class IndexExpression(BinaryExpression):
     """Represent the class of expressions that are like an "index"
     operation."""
 
-    pass
+    inherit_cache = True
 
 
 class GroupedElement(ClauseElement):
@@ -5054,14 +5056,17 @@ class _IdentifiedClause(Executable, ClauseElement):
 
 class SavepointClause(_IdentifiedClause):
     __visit_name__ = "savepoint"
+    inherit_cache = False
 
 
 class RollbackToSavepointClause(_IdentifiedClause):
     __visit_name__ = "rollback_to_savepoint"
+    inherit_cache = False
 
 
 class ReleaseSavepointClause(_IdentifiedClause):
     __visit_name__ = "release_savepoint"
+    inherit_cache = False
 
 
 class quoted_name(util.MemoizedSlots, util.text_type):
index 5729f81f512e9182a2ca3b7c3279d5b2a3e8c1c9..4f3cf65b471b4ebbe076267897d46ec92ae25d56 100644 (file)
@@ -977,6 +977,7 @@ class GenericFunction(util.with_metaclass(_GenericMeta, Function)):
 
         class as_utc(GenericFunction):
             type = DateTime
+            inherit_cache = True
 
         print(select(func.as_utc()))
 
@@ -991,6 +992,7 @@ class GenericFunction(util.with_metaclass(_GenericMeta, Function)):
         class as_utc(GenericFunction):
             type = DateTime
             package = "time"
+            inherit_cache = True
 
     The above function would be available from :data:`.func`
     using the package name ``time``::
@@ -1008,6 +1010,7 @@ class GenericFunction(util.with_metaclass(_GenericMeta, Function)):
             package = "geo"
             name = "ST_Buffer"
             identifier = "buffer"
+            inherit_cache = True
 
     The above function will render as follows::
 
@@ -1026,6 +1029,7 @@ class GenericFunction(util.with_metaclass(_GenericMeta, Function)):
             package = "geo"
             name = quoted_name("ST_Buffer", True)
             identifier = "buffer"
+            inherit_cache = True
 
     The above function will render as::
 
index 70ad4cefa7f9d153f535dd42c24e700c0f4228c2..6f25ec975703e920a4967a6236f2eed0b4c73dfa 100644 (file)
@@ -36,6 +36,11 @@ class HasCacheKeyRole(SQLRole):
     _role_name = "Cacheable Core or ORM object"
 
 
+class ExecutableOptionRole(SQLRole):
+    __slots__ = ()
+    _role_name = "ExecutionOption Core or ORM object"
+
+
 class LiteralValueRole(SQLRole):
     _role_name = "Literal Python value"
 
index 3d377271f99b8418b215751461714b5d92c19d5f..27e65652654c9d8a7f276883dab6bfb6286d50b9 100644 (file)
@@ -49,7 +49,50 @@ def _preconfigure_traversals(target_hierarchy):
 
 
 class HasCacheKey(object):
+    """Mixin for objects which can produce a cache key.
+
+    .. seealso::
+
+        :class:`.CacheKey`
+
+        :ref:`sql_caching`
+
+    """
+
     _cache_key_traversal = NO_CACHE
+
+    _is_has_cache_key = True
+
+    _hierarchy_supports_caching = True
+    """private attribute which may be set to False to prevent the
+    inherit_cache warning from being emitted for a hierarchy of subclasses.
+
+    Currently applies to the DDLElement hierarchy which does not implement
+    caching.
+
+    """
+
+    inherit_cache = None
+    """Indicate if this :class:`.HasCacheKey` instance should make use of the
+    cache key generation scheme used by its immediate superclass.
+
+    The attribute defaults to ``None``, which indicates that a construct has
+    not yet taken into account whether or not its appropriate for it to
+    participate in caching; this is functionally equivalent to setting the
+    value to ``False``, except that a warning is also emitted.
+
+    This flag can be set to ``True`` on a particular class, if the SQL that
+    corresponds to the object does not change based on attributes which
+    are local to this class, and not its superclass.
+
+    .. seealso::
+
+        :ref:`compilerext_caching` - General guideslines for setting the
+        :attr:`.HasCacheKey.inherit_cache` attribute for third-party or user
+        defined SQL constructs.
+
+    """
+
     __slots__ = ()
 
     @classmethod
@@ -60,7 +103,8 @@ class HasCacheKey(object):
         so should only be called once per class.
 
         """
-        inherit = cls.__dict__.get("inherit_cache", False)
+        inherit_cache = cls.__dict__.get("inherit_cache", None)
+        inherit = bool(inherit_cache)
 
         if inherit:
             _cache_key_traversal = getattr(cls, "_cache_key_traversal", None)
@@ -89,6 +133,23 @@ class HasCacheKey(object):
                 )
                 if _cache_key_traversal is None:
                     cls._generated_cache_key_traversal = NO_CACHE
+                    if (
+                        inherit_cache is None
+                        and cls._hierarchy_supports_caching
+                    ):
+                        util.warn(
+                            "Class %s will not make use of SQL compilation "
+                            "caching as it does not set the 'inherit_cache' "
+                            "attribute to ``True``.  This can have "
+                            "significant performance implications including "
+                            "some performance degradations in comparison to "
+                            "prior SQLAlchemy versions.  Set this attribute "
+                            "to True if this object can make use of the cache "
+                            "key generated by the superclass.  Alternatively, "
+                            "this attribute may be set to False which will "
+                            "disable this warning." % (cls.__name__),
+                            code="cprf",
+                        )
                     return NO_CACHE
 
             return _cache_key_traversal_visitor.generate_dispatch(
@@ -273,6 +334,15 @@ class MemoizedHasCacheKey(HasCacheKey, HasMemoized):
 
 
 class CacheKey(namedtuple("CacheKey", ["key", "bindparams"])):
+    """The key used to identify a SQL statement construct in the
+    SQL compilation cache.
+
+    .. seealso::
+
+        :ref:`sql_caching`
+
+    """
+
     def __hash__(self):
         """CacheKey itself is not hashable - hash the .key portion"""
 
@@ -480,7 +550,19 @@ class _CacheKey(ExtendedInternalTraversal):
             tuple(elem._gen_cache_key(anon_map, bindparams) for elem in obj),
         )
 
-    visit_executable_options = visit_has_cache_key_list
+    def visit_executable_options(
+        self, attrname, obj, parent, anon_map, bindparams
+    ):
+        if not obj:
+            return ()
+        return (
+            attrname,
+            tuple(
+                elem._gen_cache_key(anon_map, bindparams)
+                for elem in obj
+                if elem._is_has_cache_key
+            ),
+        )
 
     def visit_inspectable_list(
         self, attrname, obj, parent, anon_map, bindparams
@@ -1102,7 +1184,20 @@ class TraversalComparatorStrategy(InternalTraversal, util.MemoizedSlots):
             ):
                 return COMPARE_FAILED
 
-    visit_executable_options = visit_has_cache_key_list
+    def visit_executable_options(
+        self, attrname, left_parent, left, right_parent, right, **kw
+    ):
+        for l, r in util.zip_longest(left, right, fillvalue=None):
+            if (
+                l._gen_cache_key(self.anon_map[0], [])
+                if l._is_has_cache_key
+                else l
+            ) != (
+                r._gen_cache_key(self.anon_map[1], [])
+                if r._is_has_cache_key
+                else r
+            ):
+                return COMPARE_FAILED
 
     def visit_clauseelement(
         self, attrname, left_parent, left, right_parent, right, **kw
index 326f2be8bb50d4c97a1c32810abc1f909427536b..49f6cfe204a83bb70f9e9210937f747070802652 100644 (file)
@@ -969,18 +969,23 @@ class ExternalType(object):
 
     @property
     def _static_cache_key(self):
-        if self.cache_ok is None:
+        cache_ok = self.__class__.__dict__.get("cache_ok", None)
+
+        if cache_ok is None:
             subtype_idx = self.__class__.__mro__.index(ExternalType)
             subtype = self.__class__.__mro__[max(subtype_idx - 1, 0)]
 
             util.warn(
                 "%s %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 "
+                "the ``cache_ok`` attribute is not set to True.  This can "
+                "have significant performance implications including some "
+                "performance degradations in comparison to prior SQLAlchemy "
+                "versions.  Set this attribute to True if this type object's "
                 "state is safe to use in a cache key, or False to "
-                "disable this warning." % (subtype.__name__, self)
+                "disable this warning." % (subtype.__name__, self),
+                code="cprf",
             )
-        elif self.cache_ok is True:
+        elif cache_ok is True:
             return super(ExternalType, self)._static_cache_key
 
         return NO_CACHE
index 6bf14aecde9cdb1565db081bfa2922588825273a..ea453813a55765a67d6094e292cf888e5f448380 100644 (file)
@@ -552,6 +552,15 @@ class AssertsCompiledSQL(object):
         # are the "self.statement" element
         c = CheckCompilerAccess(clause).compile(dialect=dialect, **kw)
 
+        if isinstance(clause, sqltypes.TypeEngine):
+            cache_key_no_warnings = clause._static_cache_key
+            if cache_key_no_warnings:
+                hash(cache_key_no_warnings)
+        else:
+            cache_key_no_warnings = clause._generate_cache_key()
+            if cache_key_no_warnings:
+                hash(cache_key_no_warnings[0])
+
         param_str = repr(getattr(c, "params", {}))
         if util.py3k:
             param_str = param_str.encode("utf-8").decode("ascii", "ignore")
index 1f76e0969e743122363927c4564b4c7b32ece815..bad5e4e10b6ec07416cc74920b00e9e662c66cf1 100644 (file)
@@ -181,7 +181,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
             t.update()
             .where(t.c.somecolumn == "q")
             .values(somecolumn="x")
-            .with_hint("XYZ", "mysql"),
+            .with_hint("XYZ", dialect_name="mysql"),
             "UPDATE sometable SET somecolumn=:somecolumn "
             "WHERE sometable.somecolumn = :somecolumn_1",
         )
index bb90f66d3405c9d29ca0d9f6413cea13b78d90d3..f462a7035ccac21edf41566f3d9e0617d696ea5f 100644 (file)
@@ -3964,6 +3964,7 @@ class DialectDoesntSupportCachingTest(fixtures.TestBase):
 
         class MyDialect(SQLiteDialect_pysqlite):
             statement_compiler = MyCompiler
+            supports_statement_cache = False
 
         from sqlalchemy.dialects import registry
 
index 977fb363909b618ce8d2e87ec775273068cbcdab..a7fb1ec766ef07eae990d6cacdb63766d04db84c 100644 (file)
@@ -1043,6 +1043,7 @@ class CustomIntegrationTest(testing.AssertsCompiledSQL, BakedTest):
         from sqlalchemy.orm.interfaces import UserDefinedOption
 
         class RelationshipCache(UserDefinedOption):
+            inherit_cache = True
 
             propagate_to_loaders = True
 
index 7fb0213292c1c09d70395791c78e3f6650e31fbe..996797122000255964a7ac4c3a9c800cd2a12728 100644 (file)
@@ -37,6 +37,8 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_column(self):
         class MyThingy(ColumnClause):
+            inherit_cache = False
+
             def __init__(self, arg=None):
                 super(MyThingy, self).__init__(arg or "MYTHINGY!")
 
@@ -96,7 +98,7 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_no_compile_for_col_label(self):
         class MyThingy(FunctionElement):
-            pass
+            inherit_cache = True
 
         @compiles(MyThingy)
         def visit_thingy(thingy, compiler, **kw):
@@ -120,6 +122,8 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_stateful(self):
         class MyThingy(ColumnClause):
+            inherit_cache = False
+
             def __init__(self):
                 super(MyThingy, self).__init__("MYTHINGY!")
 
@@ -142,6 +146,8 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_callout_to_compiler(self):
         class InsertFromSelect(ClauseElement):
+            inherit_cache = False
+
             def __init__(self, table, select):
                 self.table = table
                 self.select = select
@@ -162,7 +168,7 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_no_default_but_has_a_visit(self):
         class MyThingy(ColumnClause):
-            pass
+            inherit_cache = False
 
         @compiles(MyThingy, "postgresql")
         def visit_thingy(thingy, compiler, **kw):
@@ -172,7 +178,7 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_no_default_has_no_visit(self):
         class MyThingy(TypeEngine):
-            pass
+            inherit_cache = False
 
         @compiles(MyThingy, "postgresql")
         def visit_thingy(thingy, compiler, **kw):
@@ -189,6 +195,7 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
     @testing.combinations((True,), (False,))
     def test_no_default_proxy_generation(self, named):
         class my_function(FunctionElement):
+            inherit_cache = False
             if named:
                 name = "my_function"
             type = Numeric()
@@ -215,7 +222,7 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_no_default_message(self):
         class MyThingy(ClauseElement):
-            pass
+            inherit_cache = False
 
         @compiles(MyThingy, "postgresql")
         def visit_thingy(thingy, compiler, **kw):
@@ -314,7 +321,7 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
         from sqlalchemy.dialects import postgresql
 
         class MyUtcFunction(FunctionElement):
-            pass
+            inherit_cache = True
 
         @compiles(MyUtcFunction)
         def visit_myfunc(element, compiler, **kw):
@@ -335,7 +342,7 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_functions_args_noname(self):
         class myfunc(FunctionElement):
-            pass
+            inherit_cache = True
 
         @compiles(myfunc)
         def visit_myfunc(element, compiler, **kw):
@@ -351,6 +358,7 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
         class greatest(FunctionElement):
             type = Numeric()
             name = "greatest"
+            inherit_cache = True
 
         @compiles(greatest)
         def default_greatest(element, compiler, **kw):
@@ -380,12 +388,15 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_function_subclasses_one(self):
         class Base(FunctionElement):
+            inherit_cache = True
             name = "base"
 
         class Sub1(Base):
+            inherit_cache = True
             name = "sub1"
 
         class Sub2(Base):
+            inherit_cache = True
             name = "sub2"
 
         @compiles(Base)
@@ -407,6 +418,7 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
             name = "base"
 
         class Sub1(Base):
+            inherit_cache = True
             name = "sub1"
 
         @compiles(Base)
@@ -414,9 +426,11 @@ class UserDefinedTest(fixtures.TestBase, AssertsCompiledSQL):
             return element.name
 
         class Sub2(Base):
+            inherit_cache = True
             name = "sub2"
 
         class SubSub1(Sub1):
+            inherit_cache = True
             name = "subsub1"
 
         self.assert_compile(
@@ -545,7 +559,7 @@ class ExecuteTest(fixtures.TablesTest):
     @testing.fixture()
     def insert_fixture(self):
         class MyInsert(Executable, ClauseElement):
-            pass
+            inherit_cache = True
 
         @compiles(MyInsert)
         def _run_myinsert(element, compiler, **kw):
@@ -556,7 +570,7 @@ class ExecuteTest(fixtures.TablesTest):
     @testing.fixture()
     def select_fixture(self):
         class MySelect(Executable, ClauseElement):
-            pass
+            inherit_cache = True
 
         @compiles(MySelect)
         def _run_myinsert(element, compiler, **kw):
index 729e1ee0479578847b1cb62a562776b7e5ff9b4d..3d17d702382c96f2862dd5cee08bb0ffe36d5491 100644 (file)
@@ -2252,7 +2252,7 @@ class ColSubclassTest(
             id = Column(Integer, primary_key=True)
 
         class MySpecialColumn(Column):
-            pass
+            inherit_cache = True
 
         class B(A):
             __tablename__ = "b"
index 7fb232b0b8761b53e71e3816581ff6c8a4372992..f42e59216a00c83645a44eef08343d69c824dabd 100644 (file)
@@ -26,6 +26,7 @@ from sqlalchemy.orm import relationship
 from sqlalchemy.orm import selectinload
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import subqueryload
+from sqlalchemy.orm import synonym
 from sqlalchemy.orm import with_expression
 from sqlalchemy.orm import with_loader_criteria
 from sqlalchemy.orm import with_polymorphic
@@ -387,6 +388,35 @@ class CacheKeyTest(CacheKeyFixture, _fixtures.FixtureTest):
             compare_values=True,
         )
 
+    def test_synonyms(self, registry):
+        """test for issue discovered in #7394"""
+
+        @registry.mapped
+        class User2(object):
+            __table__ = self.tables.users
+
+            name_syn = synonym("name")
+
+        @registry.mapped
+        class Address2(object):
+            __table__ = self.tables.addresses
+
+            name_syn = synonym("email_address")
+
+        self._run_cache_key_fixture(
+            lambda: (
+                User2.id,
+                User2.name,
+                User2.name_syn,
+                Address2.name_syn,
+                Address2.email_address,
+                aliased(User2).name_syn,
+                aliased(User2, name="foo").name_syn,
+                aliased(User2, name="bar").name_syn,
+            ),
+            compare_values=True,
+        )
+
     def test_more_with_entities_sanity_checks(self):
         """test issue #6503"""
         User, Address, Keyword, Order, Item = self.classes(
index 5274271d9facc679f1b7e444320a18026f8b11eb..6de702ad4b0cd529cc05a2911e6daf583cec5c70 100644 (file)
@@ -219,7 +219,7 @@ class LambdaTest(QueryTest, AssertsCompiledSQL):
 
         assert_raises_message(
             exc.ArgumentError,
-            "Cacheable Core or ORM object expected, got",
+            "ExecutionOption Core or ORM object expected, got",
             select(lambda: User).options,
             lambda: subqueryload(User.addresses),
         )
index 433c11afc90fc4fc6af5efe0642041ec30cd7631..8bf3dcdb5c5f8373f5ace07cd95d231a6e5b2298 100644 (file)
@@ -2113,6 +2113,7 @@ class ExpressionTest(QueryTest, AssertsCompiledSQL):
 
         class max_(expression.FunctionElement):
             name = "max"
+            inherit_cache = True
 
         @compiles(max_)
         def visit_max(element, compiler, **kw):
@@ -2127,6 +2128,7 @@ class ExpressionTest(QueryTest, AssertsCompiledSQL):
 
         class not_named_max(expression.ColumnElement):
             name = "not_named_max"
+            inherit_cache = True
 
         @compiles(not_named_max)
         def visit_max(element, compiler, **kw):
index fe3512375ac3723da118525cc902ca77b63dfc31..a5252601c42b2642084a5e54ffd576c8097c9670 100644 (file)
@@ -16,6 +16,7 @@ from sqlalchemy import Integer
 from sqlalchemy import literal_column
 from sqlalchemy import MetaData
 from sqlalchemy import or_
+from sqlalchemy import PickleType
 from sqlalchemy import select
 from sqlalchemy import String
 from sqlalchemy import Table
@@ -1265,13 +1266,20 @@ class CacheKeyTest(CacheKeyFixture, CoreFixtures, fixtures.TestBase):
         # the None for cache key will prevent objects
         # which contain these elements from being cached.
         f1 = Foobar1()
-        eq_(f1._generate_cache_key(), None)
+        with expect_warnings(
+            "Class Foobar1 will not make use of SQL compilation caching"
+        ):
+            eq_(f1._generate_cache_key(), None)
 
         f2 = Foobar2()
-        eq_(f2._generate_cache_key(), None)
+        with expect_warnings(
+            "Class Foobar2 will not make use of SQL compilation caching"
+        ):
+            eq_(f2._generate_cache_key(), None)
 
         s1 = select(column("q"), Foobar2())
 
+        # warning is memoized, won't happen the second time
         eq_(s1._generate_cache_key(), None)
 
     def test_get_children_no_method(self):
@@ -1356,6 +1364,7 @@ class CompareAndCopyTest(CoreFixtures, fixtures.TestBase):
             and (
                 "__init__" in cls.__dict__
                 or issubclass(cls, AliasedReturnsRows)
+                or "inherit_cache" not in cls.__dict__
             )
             and not issubclass(cls, (Annotated))
             and "orm" not in cls.__module__
@@ -1820,3 +1829,69 @@ class TypesTest(fixtures.TestBase):
         eq_(c1, c2)
         ne_(c1, c3)
         eq_(c1, c4)
+
+    def test_thirdparty_sub_subclass_no_cache(self):
+        class MyType(PickleType):
+            pass
+
+        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_userdefined_sub_subclass_no_cache(self):
+        class MyType(UserDefinedType):
+            cache_ok = True
+
+        class MySubType(MyType):
+            pass
+
+        expr = column("q", MySubType()) == 1
+
+        with expect_warnings(
+            r"UserDefinedType MySubType\(\) will not produce a cache key"
+        ):
+            is_(expr._generate_cache_key(), None)
+
+    def test_userdefined_sub_subclass_cache_ok(self):
+        class MyType(UserDefinedType):
+            cache_ok = True
+
+        class MySubType(MyType):
+            cache_ok = True
+
+        def go1():
+            expr = column("q", MySubType()) == 1
+            return expr
+
+        def go2():
+            expr = column("p", MySubType()) == 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_thirdparty_sub_subclass_cache_ok(self):
+        class MyType(PickleType):
+            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)
index f3fb724c073fd3eae8a7058f4a3c7d9fe49ce171..27f1b8974208070b787fffc3be9e49f477abe1a3 100644 (file)
@@ -86,6 +86,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
 
             # test generic function compile
             class fake_func(GenericFunction):
+                inherit_cache = True
                 __return_type__ = sqltypes.Integer
 
                 def __init__(self, arg, **kwargs):
@@ -112,6 +113,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
         if use_custom:
 
             class MyFunc(FunctionElement):
+                inherit_cache = True
                 name = "myfunc"
                 type = Integer()
 
@@ -140,6 +142,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_use_labels_function_element(self):
         class max_(FunctionElement):
             name = "max"
+            inherit_cache = True
 
         @compiles(max_)
         def visit_max(element, compiler, **kw):
@@ -265,7 +268,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_custom_default_namespace(self):
         class myfunc(GenericFunction):
-            pass
+            inherit_cache = True
 
         assert isinstance(func.myfunc(), myfunc)
         self.assert_compile(func.myfunc(), "myfunc()")
@@ -273,6 +276,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_custom_type(self):
         class myfunc(GenericFunction):
             type = DateTime
+            inherit_cache = True
 
         assert isinstance(func.myfunc().type, DateTime)
         self.assert_compile(func.myfunc(), "myfunc()")
@@ -280,12 +284,14 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_custom_legacy_type(self):
         # in case someone was using this system
         class myfunc(GenericFunction):
+            inherit_cache = True
             __return_type__ = DateTime
 
         assert isinstance(func.myfunc().type, DateTime)
 
     def test_case_sensitive(self):
         class MYFUNC(GenericFunction):
+            inherit_cache = True
             type = DateTime
 
         assert isinstance(func.MYFUNC().type, DateTime)
@@ -341,6 +347,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_custom_w_custom_name(self):
         class myfunc(GenericFunction):
+            inherit_cache = True
             name = "notmyfunc"
 
         assert isinstance(func.notmyfunc(), myfunc)
@@ -348,6 +355,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_custom_w_quoted_name(self):
         class myfunc(GenericFunction):
+            inherit_cache = True
             name = quoted_name("NotMyFunc", quote=True)
             identifier = "myfunc"
 
@@ -355,6 +363,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_custom_w_quoted_name_no_identifier(self):
         class myfunc(GenericFunction):
+            inherit_cache = True
             name = quoted_name("NotMyFunc", quote=True)
 
         # note this requires that the quoted name be lower cased for
@@ -364,6 +373,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_custom_package_namespace(self):
         def cls1(pk_name):
             class myfunc(GenericFunction):
+                inherit_cache = True
                 package = pk_name
 
             return myfunc
@@ -377,6 +387,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
     def test_custom_name(self):
         class MyFunction(GenericFunction):
             name = "my_func"
+            inherit_cache = True
 
             def __init__(self, *args):
                 args = args + (3,)
@@ -392,20 +403,24 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
             package = "geo"
             name = "BufferOne"
             identifier = "buf1"
+            inherit_cache = True
 
         class GeoBuffer2(GenericFunction):
             type = Integer
             name = "BufferTwo"
             identifier = "buf2"
+            inherit_cache = True
 
         class BufferThree(GenericFunction):
             type = Integer
             identifier = "buf3"
+            inherit_cache = True
 
         class GeoBufferFour(GenericFunction):
             type = Integer
             name = "BufferFour"
             identifier = "Buf4"
+            inherit_cache = True
 
         self.assert_compile(func.geo.buf1(), "BufferOne()")
         self.assert_compile(func.buf2(), "BufferTwo()")
@@ -418,7 +433,7 @@ class CompileTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def test_custom_args(self):
         class myfunc(GenericFunction):
-            pass
+            inherit_cache = True
 
         self.assert_compile(
             myfunc(1, 2, 3), "myfunc(:myfunc_1, :myfunc_2, :myfunc_3)"
@@ -1015,6 +1030,7 @@ class ExecuteTest(fixtures.TestBase):
         from sqlalchemy.ext.compiler import compiles
 
         class myfunc(FunctionElement):
+            inherit_cache = True
             type = Date()
 
         @compiles(myfunc)
index 535d4dd0be8d8404ee8db42d727572a584c0181d..8c8e9dbeda358b8ecefb590e4658b85feb178970 100644 (file)
@@ -805,6 +805,8 @@ class ColExprLabelTest(fixtures.TestBase, AssertsCompiledSQL):
 
     def _fixture(self):
         class SomeColThing(WrapsColumnExpression, ColumnElement):
+            inherit_cache = False
+
             def __init__(self, expression):
                 self.clause = coercions.expect(
                     roles.ExpressionElementRole, expression
index a2aa9705cb16c63509fa135de6d1577722452640..76be0af3cea92481536be54ac5d86f3bac188a5c 100644 (file)
@@ -17,6 +17,7 @@ from sqlalchemy.sql import roles
 from sqlalchemy.sql import select
 from sqlalchemy.sql import table
 from sqlalchemy.sql import util as sql_util
+from sqlalchemy.sql.base import ExecutableOption
 from sqlalchemy.sql.traversals import HasCacheKey
 from sqlalchemy.testing import assert_raises_message
 from sqlalchemy.testing import AssertsCompiledSQL
@@ -810,7 +811,10 @@ class LambdaElementTest(
 
         stmt = lambdas.lambda_stmt(lambda: select(column("x")))
 
-        opts = {column("x"), column("y")}
+        class MyUncacheable(ExecutableOption):
+            pass
+
+        opts = {MyUncacheable()}
 
         assert_raises_message(
             exc.InvalidRequestError,
@@ -942,11 +946,18 @@ class LambdaElementTest(
 
             return stmt
 
-        s1 = go([column("a"), column("b")])
+        class SomeOpt(HasCacheKey, ExecutableOption):
+            def __init__(self, x):
+                self.x = x
+
+            def _gen_cache_key(self, anon_map, bindparams):
+                return (SomeOpt, self.x)
+
+        s1 = go([SomeOpt("a"), SomeOpt("b")])
 
-        s2 = go([column("a"), column("b")])
+        s2 = go([SomeOpt("a"), SomeOpt("b")])
 
-        s3 = go([column("q"), column("b")])
+        s3 = go([SomeOpt("q"), SomeOpt("b")])
 
         s1key = s1._generate_cache_key()
         s2key = s2._generate_cache_key()
@@ -964,7 +975,7 @@ class LambdaElementTest(
 
             return stmt
 
-        class SomeOpt(HasCacheKey):
+        class SomeOpt(HasCacheKey, ExecutableOption):
             def _gen_cache_key(self, anon_map, bindparams):
                 return ("fixed_key",)
 
@@ -994,8 +1005,8 @@ class LambdaElementTest(
 
             return stmt
 
-        class SomeOpt(HasCacheKey):
-            pass
+        class SomeOpt(HasCacheKey, ExecutableOption):
+            inherit_cache = False
 
         # generates no key, will not be cached
         eq_(SomeOpt()._generate_cache_key(), None)
index 0e6f4f2d96c2699cd49572cb036652c79cfbae17..c04078f7372f5ad9bf6a122a422e185991a21e62 100644 (file)
@@ -656,6 +656,8 @@ class ExtensionOperatorTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     def test_contains(self):
         class MyType(UserDefinedType):
+            cache_ok = True
+
             class comparator_factory(UserDefinedType.Comparator):
                 def contains(self, other, **kw):
                     return self.op("->")(other)
@@ -664,6 +666,8 @@ class ExtensionOperatorTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     def test_getitem(self):
         class MyType(UserDefinedType):
+            cache_ok = True
+
             class comparator_factory(UserDefinedType.Comparator):
                 def __getitem__(self, index):
                     return self.op("->")(index)
@@ -682,6 +686,8 @@ class ExtensionOperatorTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     def test_lshift(self):
         class MyType(UserDefinedType):
+            cache_ok = True
+
             class comparator_factory(UserDefinedType.Comparator):
                 def __lshift__(self, other):
                     return self.op("->")(other)
@@ -690,6 +696,8 @@ class ExtensionOperatorTest(fixtures.TestBase, testing.AssertsCompiledSQL):
 
     def test_rshift(self):
         class MyType(UserDefinedType):
+            cache_ok = True
+
             class comparator_factory(UserDefinedType.Comparator):
                 def __rshift__(self, other):
                     return self.op("->")(other)
index d07f81facee01199b52f154a3600182ed06e83c9..89317b149ffbf0184ca708addaa57be555bd543a 100644 (file)
@@ -1798,6 +1798,7 @@ class KeyTargetingTest(fixtures.TablesTest):
     def test_keyed_targeting_no_label_at_all_one(self, connection):
         class not_named_max(expression.ColumnElement):
             name = "not_named_max"
+            inherit_cache = True
 
         @compiles(not_named_max)
         def visit_max(element, compiler, **kw):
@@ -1815,6 +1816,7 @@ class KeyTargetingTest(fixtures.TablesTest):
     def test_keyed_targeting_no_label_at_all_two(self, connection):
         class not_named_max(expression.ColumnElement):
             name = "not_named_max"
+            inherit_cache = True
 
         @compiles(not_named_max)
         def visit_max(element, compiler, **kw):
index ffa6f922ed282df79bfa8cead0f26f8904e9f8f4..14b1ca1051ba2644a4974e83723b2c47bff704a1 100644 (file)
@@ -1550,6 +1550,8 @@ class VariantTest(fixtures.TestBase, AssertsCompiledSQL):
                 return process
 
         class UTypeThree(types.UserDefinedType):
+            cache_ok = True
+
             def get_col_spec(self):
                 return "UTYPETHREE"