Skip to content

Commit

Permalink
Warn when caching is disabled / document
Browse files Browse the repository at this point in the history
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
  • Loading branch information
zzzeek committed Dec 6, 2021
1 parent e88dc00 commit 22deafe
Show file tree
Hide file tree
Showing 45 changed files with 979 additions and 78 deletions.
49 changes: 49 additions & 0 deletions doc/build/changelog/unreleased_14/7394.rst
Original file line number Diff line number Diff line change
@@ -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.
127 changes: 113 additions & 14 deletions doc/build/core/connections.rst
Original file line number Diff line number Diff line change
Expand Up @@ -839,6 +839,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
------------------------------------------

Expand Down Expand Up @@ -1106,28 +1108,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

Expand All @@ -1141,6 +1150,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:

Expand Down
1 change: 1 addition & 0 deletions doc/build/core/expression_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ see :ref:`sqlexpression_toplevel`.
.. toctree::
:maxdepth: 3

foundation
sqlelement
operators
selectable
Expand Down
32 changes: 32 additions & 0 deletions doc/build/core/foundation.rst
Original file line number Diff line number Diff line change
@@ -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:

16 changes: 0 additions & 16 deletions doc/build/core/sqlelement.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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:
Expand All @@ -170,9 +160,6 @@ The classes here are generated using the constructors listed at
.. autoclass:: Label
:members:

.. autoclass:: LambdaElement
:members:

.. autoclass:: Null
:members:

Expand All @@ -183,9 +170,6 @@ The classes here are generated using the constructors listed at
.. autoclass:: Over
:members:

.. autoclass:: StatementLambdaElement
:members:

.. autoclass:: TextClause
:members:

Expand Down
3 changes: 2 additions & 1 deletion doc/build/core/visitors.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,5 @@ as well as when building out custom SQL expressions using the

.. automodule:: sqlalchemy.sql.visitors
:members:
:private-members:
:private-members:

0 comments on commit 22deafe

Please sign in to comment.