From: Mike Bayer Date: Tue, 7 Oct 2025 18:55:44 +0000 (-0400) Subject: Add Executable traverse internals to Executable subclasses and turn tests on X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=148059ced691da5edf3504a1d999cb1ab638dc7b;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git Add Executable traverse internals to Executable subclasses and turn tests on Fixed a caching issue where :func:`_orm.with_loader_criteria` would incorrectly reuse cached bound parameter values when used with :class:`_sql.CompoundSelect` constructs such as :func:`_sql.union`. The issue was caused by the cache key for compound selects not including the execution options that are part of the :class:`_sql.Executable` base class, which :func:`_orm.with_loader_criteria` uses to apply its criteria dynamically. The fix ensures that compound selects and other executable constructs properly include execution options in their cache key traversal. Fixes: #12905 Change-Id: I24bbd96233cddabe42eb716f078eea4c84b1af05 --- diff --git a/doc/build/changelog/unreleased_20/12905.rst b/doc/build/changelog/unreleased_20/12905.rst new file mode 100644 index 0000000000..e99a14e66c --- /dev/null +++ b/doc/build/changelog/unreleased_20/12905.rst @@ -0,0 +1,12 @@ +.. change:: + :tags: bug, orm + :tickets: 12905 + + Fixed a caching issue where :func:`_orm.with_loader_criteria` would + incorrectly reuse cached bound parameter values when used with + :class:`_sql.CompoundSelect` constructs such as :func:`_sql.union`. The + issue was caused by the cache key for compound selects not including the + execution options that are part of the :class:`_sql.Executable` base class, + which :func:`_orm.with_loader_criteria` uses to apply its criteria + dynamically. The fix ensures that compound selects and other executable + constructs properly include execution options in their cache key traversal. diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index e8a830b2b4..39614b9178 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -2340,7 +2340,7 @@ class TextClause( _traverse_internals: _TraverseInternalsType = [ ("_bindparams", InternalTraversal.dp_string_clauseelement_dict), ("text", InternalTraversal.dp_string), - ] + ] + Executable._executable_traverse_internals _is_text_clause = True diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py index 0230851227..dda890c0b5 100644 --- a/lib/sqlalchemy/sql/functions.py +++ b/lib/sqlalchemy/sql/functions.py @@ -138,7 +138,7 @@ class FunctionElement(Executable, ColumnElement[_T], FromClause, Generative): ("clause_expr", InternalTraversal.dp_clauseelement), ("_with_ordinality", InternalTraversal.dp_boolean), ("_table_value_type", InternalTraversal.dp_has_cache_key), - ] + ] + Executable._executable_traverse_internals packagenames: Tuple[str, ...] = () diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 347ba0e144..60f062ec95 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -4513,6 +4513,7 @@ class CompoundSelect( + SupportsCloneAnnotations._clone_annotations_traverse_internals + HasCTE._has_ctes_traverse_internals + DialectKWArgs._dialect_kwargs_traverse_internals + + Executable._executable_traverse_internals ) selects: List[SelectBase] @@ -7166,6 +7167,7 @@ class TextualSelect(SelectBase, ExecutableReturnsRows, Generative): ] + SupportsCloneAnnotations._clone_annotations_traverse_internals + HasCTE._has_ctes_traverse_internals + + Executable._executable_traverse_internals ) _is_textual = True diff --git a/test/sql/test_compare.py b/test/sql/test_compare.py index 45cea0c46a..774f9792f4 100644 --- a/test/sql/test_compare.py +++ b/test/sql/test_compare.py @@ -36,6 +36,7 @@ from sqlalchemy.sql import aggregate_order_by from sqlalchemy.sql import bindparam from sqlalchemy.sql import ColumnElement from sqlalchemy.sql import dml +from sqlalchemy.sql import Executable from sqlalchemy.sql import False_ from sqlalchemy.sql import func from sqlalchemy.sql import operators @@ -1538,10 +1539,6 @@ class HasCacheKeySubclass(fixtures.TestBase): super_traverse = {} # ignore_super = self.ignore_super.get(cls.__name__, set()) for s in cls.mro()[1:]: - # if s.__name__ in ignore_super: - # continue - if s.__name__ == "Executable": - continue for attr in s.__dict__: if not attr.endswith("_traverse_internals"): continue @@ -1724,6 +1721,7 @@ class HasCacheKeySubclass(fixtures.TestBase): SingletonConstant, SyntaxExtension, DialectKWArgs, + Executable, ] ) )