summaryrefslogtreecommitdiff
path: root/doc/build/core/connections.rst
diff options
context:
space:
mode:
Diffstat (limited to 'doc/build/core/connections.rst')
-rw-r--r--doc/build/core/connections.rst127
1 files changed, 113 insertions, 14 deletions
diff --git a/doc/build/core/connections.rst b/doc/build/core/connections.rst
index 08e782351..3de493686 100644
--- a/doc/build/core/connections.rst
+++ b/doc/build/core/connections.rst
@@ -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
------------------------------------------
@@ -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
@@ -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: