]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
adjust bound parameters within cache key only, dont deep copy
authorMike Bayer <mike_mp@zzzcomputing.com>
Sat, 2 Mar 2024 05:28:26 +0000 (00:28 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 3 Mar 2024 23:23:26 +0000 (18:23 -0500)
Adjusted the fix made in :ticket:`10570`, released in 2.0.23, where new
logic was added to reconcile possibly changing bound parameter values
across cache key generations used within the :func:`_orm.with_expression`
construct.  The new logic changes the approach by which the new bound
parameter values are associated with the statement, avoiding the need to
deep-copy the statement which can result in a significant performance
penalty for very deep / complex SQL constructs.  The new approach no longer
requires this deep-copy step.

Fixes: #11085
Change-Id: Ia51eb4e949c8f37af135399925a9916b9ed4ad2f

doc/build/changelog/unreleased_20/11085.rst [new file with mode: 0644]
lib/sqlalchemy/orm/strategy_options.py
lib/sqlalchemy/sql/cache_key.py
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/elements.py
test/aaa_profiling/test_orm.py
test/profiles.txt
test/sql/test_compare.py

diff --git a/doc/build/changelog/unreleased_20/11085.rst b/doc/build/changelog/unreleased_20/11085.rst
new file mode 100644 (file)
index 0000000..74f877d
--- /dev/null
@@ -0,0 +1,12 @@
+.. change::
+    :tags: bug, orm, performance, regression
+    :tickets: 11085
+
+    Adjusted the fix made in :ticket:`10570`, released in 2.0.23, where new
+    logic was added to reconcile possibly changing bound parameter values
+    across cache key generations used within the :func:`_orm.with_expression`
+    construct.  The new logic changes the approach by which the new bound
+    parameter values are associated with the statement, avoiding the need to
+    deep-copy the statement which can result in a significant performance
+    penalty for very deep / complex SQL constructs.  The new approach no longer
+    requires this deep-copy step.
index d69fa6edb41d1caa388284b1ebbf60489ba3574a..36ccc479d0b349164b27c66f5df51f564a9286f4 100644 (file)
@@ -1034,6 +1034,8 @@ class Load(_AbstractLoad):
     def _adapt_cached_option_to_uncached_option(
         self, context: QueryContext, uncached_opt: ORMOption
     ) -> ORMOption:
+        if uncached_opt is self:
+            return self
         return self._adjust_for_extra_criteria(context)
 
     def _prepend_path(self, path: PathRegistry) -> Load:
@@ -1049,47 +1051,51 @@ class Load(_AbstractLoad):
         returning a new instance of this ``Load`` object.
 
         """
-        orig_query = context.compile_state.select_statement
-
-        orig_cache_key: Optional[CacheKey] = None
-        replacement_cache_key: Optional[CacheKey] = None
-        found_crit = False
 
-        def process(opt: _LoadElement) -> _LoadElement:
-            nonlocal orig_cache_key, replacement_cache_key, found_crit
-
-            found_crit = True
+        # avoid generating cache keys for the queries if we don't
+        # actually have any extra_criteria options, which is the
+        # common case
+        for value in self.context:
+            if value._extra_criteria:
+                break
+        else:
+            return self
 
-            if orig_cache_key is None or replacement_cache_key is None:
-                orig_cache_key = orig_query._generate_cache_key()
-                replacement_cache_key = context.query._generate_cache_key()
+        replacement_cache_key = context.query._generate_cache_key()
 
-            if replacement_cache_key is not None:
-                assert orig_cache_key is not None
+        if replacement_cache_key is None:
+            return self
 
-                opt._extra_criteria = tuple(
-                    replacement_cache_key._apply_params_to_element(
-                        orig_cache_key, crit
-                    )
-                    for crit in opt._extra_criteria
+        orig_query = context.compile_state.select_statement
+        orig_cache_key = orig_query._generate_cache_key()
+        assert orig_cache_key is not None
+
+        def process(
+            opt: _LoadElement,
+            replacement_cache_key: CacheKey,
+            orig_cache_key: CacheKey,
+        ) -> _LoadElement:
+            cloned_opt = opt._clone()
+
+            cloned_opt._extra_criteria = tuple(
+                replacement_cache_key._apply_params_to_element(
+                    orig_cache_key, crit
                 )
+                for crit in cloned_opt._extra_criteria
+            )
 
-            return opt
+            return cloned_opt
 
-        # avoid generating cache keys for the queries if we don't
-        # actually have any extra_criteria options, which is the
-        # common case
-        new_context = tuple(
-            process(value._clone()) if value._extra_criteria else value
+        cloned = self._clone()
+        cloned.context = tuple(
+            (
+                process(value, replacement_cache_key, orig_cache_key)
+                if value._extra_criteria
+                else value
+            )
             for value in self.context
         )
-
-        if found_crit:
-            cloned = self._clone()
-            cloned.context = new_context
-            return cloned
-        else:
-            return self
+        return cloned
 
     def _reconcile_query_entities_with_us(self, mapper_entities, raiseerr):
         """called at process time to allow adjustment of the root
index ba8a5403e7e433fefe5940617258c70d8acef87b..d59958bf262fc7061718b9a6df530e6041e0f4d8 100644 (file)
@@ -37,6 +37,7 @@ from ..util.typing import Literal
 if typing.TYPE_CHECKING:
     from .elements import BindParameter
     from .elements import ClauseElement
+    from .elements import ColumnElement
     from .visitors import _TraverseInternalsType
     from ..engine.interfaces import _CoreSingleExecuteParams
 
@@ -557,18 +558,17 @@ class CacheKey(NamedTuple):
         _anon_map = prefix_anon_map()
         return {b.key % _anon_map: b.effective_value for b in self.bindparams}
 
+    @util.preload_module("sqlalchemy.sql.elements")
     def _apply_params_to_element(
-        self, original_cache_key: CacheKey, target_element: ClauseElement
-    ) -> ClauseElement:
-        if target_element._is_immutable:
+        self, original_cache_key: CacheKey, target_element: ColumnElement[Any]
+    ) -> ColumnElement[Any]:
+        if target_element._is_immutable or original_cache_key is self:
             return target_element
 
-        translate = {
-            k.key: v.value
-            for k, v in zip(original_cache_key.bindparams, self.bindparams)
-        }
-
-        return target_element.params(translate)
+        elements = util.preloaded.sql_elements
+        return elements._OverrideBinds(
+            target_element, self.bindparams, original_cache_key.bindparams
+        )
 
 
 def _ad_hoc_cache_key_from_args(
index 4c30b9363829b31d22102f4dd41a40fb38004d0b..f0b45f8b1afc77a5696ff7103b42ad1789219cb5 100644 (file)
@@ -2385,6 +2385,47 @@ class SQLCompiler(Compiled):
         """
         return ""
 
+    def visit_override_binds(self, override_binds, **kw):
+        """SQL compile the nested element of an _OverrideBinds with
+        bindparams swapped out.
+
+        The _OverrideBinds is not normally expected to be compiled; it
+        is meant to be used when an already cached statement is to be used,
+        the compilation was already performed, and only the bound params should
+        be swapped in at execution time.
+
+        However, the test suite has some tests that exercise compilation
+        on individual elements without using the cache key version, so here we
+        modify the bound parameter collection for the given compiler based on
+        the translation.
+
+        """
+
+        # get SQL text first
+        sqltext = override_binds.element._compiler_dispatch(self, **kw)
+
+        # then change binds after the fact.  note that we don't try to
+        # swap the bindparam as we compile, because our element may be
+        # elsewhere in the statement already (e.g. a subquery or perhaps a
+        # CTE) and was already visited / compiled. See
+        # test_relationship_criteria.py ->
+        #    test_selectinload_local_criteria_subquery
+        for k in override_binds.translate:
+            if k not in self.binds:
+                continue
+            bp = self.binds[k]
+
+            new_bp = bp._with_value(
+                override_binds.translate[bp.key],
+                maintain_key=True,
+                required=False,
+            )
+            name = self.bind_names[bp]
+            self.binds[k] = self.binds[name] = new_bp
+            self.bind_names[new_bp] = name
+
+        return sqltext
+
     def visit_grouping(self, grouping, asfrom=False, **kwargs):
         return "(" + grouping.element._compiler_dispatch(self, **kwargs) + ")"
 
@@ -3616,6 +3657,7 @@ class SQLCompiler(Compiled):
         render_postcompile=False,
         **kwargs,
     ):
+
         if not skip_bind_expression:
             impl = bindparam.type.dialect_impl(self.dialect)
             if impl._has_bind_expression:
index bf7e9438d9b99ea71583363b0bf5eabd8e99aec6..98f45d9dbf76ba745cc8a2820624d588529baa48 100644 (file)
@@ -106,6 +106,7 @@ if typing.TYPE_CHECKING:
     from .type_api import TypeEngine
     from .visitors import _CloneCallableType
     from .visitors import _TraverseInternalsType
+    from .visitors import anon_map
     from ..engine import Connection
     from ..engine import Dialect
     from ..engine import Engine
@@ -4068,6 +4069,57 @@ class Grouping(GroupedElement, ColumnElement[_T]):
         self.type = state["type"]
 
 
+class _OverrideBinds(Grouping[_T]):
+    """used by cache_key->_apply_params_to_element to allow compilation /
+    execution of a SQL element that's been cached, using an alternate set of
+    bound parameter values.
+
+    This is used by the ORM to swap new parameter values into expressions
+    that are embedded into loader options like with_expression(),
+    selectinload().  Previously, this task was accomplished using the
+    .params() method which would perform a deep-copy instead.  This deep
+    copy proved to be too expensive for more complex expressions.
+
+    See #11085
+
+    """
+
+    __visit_name__ = "override_binds"
+
+    def __init__(
+        self,
+        element: ColumnElement[_T],
+        bindparams: Sequence[BindParameter[Any]],
+        replaces_params: Sequence[BindParameter[Any]],
+    ):
+        self.element = element
+        self.translate = {
+            k.key: v.value for k, v in zip(replaces_params, bindparams)
+        }
+
+    def _gen_cache_key(
+        self, anon_map: anon_map, bindparams: List[BindParameter[Any]]
+    ) -> Optional[typing_Tuple[Any, ...]]:
+        """generate a cache key for the given element, substituting its bind
+        values for the translation values present."""
+
+        existing_bps: List[BindParameter[Any]] = []
+        ck = self.element._gen_cache_key(anon_map, existing_bps)
+
+        bindparams.extend(
+            (
+                bp._with_value(
+                    self.translate[bp.key], maintain_key=True, required=False
+                )
+                if bp.key in self.translate
+                else bp
+            )
+            for bp in existing_bps
+        )
+
+        return ck
+
+
 class _OverRange(IntEnum):
     RANGE_UNBOUNDED = 0
     RANGE_CURRENT = 1
index 8bf2bfa1803c40e1469ddab8c21e0a6ac10f1fd0..e02c7cae857e83df74113e35034494b7ec2e634e 100644 (file)
@@ -1,7 +1,9 @@
 from sqlalchemy import and_
 from sqlalchemy import ForeignKey
+from sqlalchemy import Identity
 from sqlalchemy import Integer
 from sqlalchemy import join
+from sqlalchemy import literal_column
 from sqlalchemy import select
 from sqlalchemy import String
 from sqlalchemy import testing
@@ -13,10 +15,12 @@ from sqlalchemy.orm import defer
 from sqlalchemy.orm import join as orm_join
 from sqlalchemy.orm import joinedload
 from sqlalchemy.orm import Load
+from sqlalchemy.orm import query_expression
 from sqlalchemy.orm import relationship
 from sqlalchemy.orm import selectinload
 from sqlalchemy.orm import Session
 from sqlalchemy.orm import sessionmaker
+from sqlalchemy.orm import with_expression
 from sqlalchemy.testing import fixtures
 from sqlalchemy.testing import profiling
 from sqlalchemy.testing.fixtures import fixture_session
@@ -1314,3 +1318,112 @@ class AnnotatedOverheadTest(NoCache, fixtures.MappedTest):
                 r = q.all()  # noqa: F841
 
         go()
+
+
+class WithExpresionLoaderOptTest(fixtures.DeclarativeMappedTest):
+    # keep caching on with this test.
+    __requires__ = ("python_profiling_backend",)
+
+    """test #11085"""
+
+    @classmethod
+    def setup_classes(cls):
+        Base = cls.DeclarativeBasic
+
+        class A(Base):
+            __tablename__ = "a"
+
+            id = Column(Integer, Identity(), primary_key=True)
+            data = Column(String(30))
+            bs = relationship("B")
+
+        class B(Base):
+            __tablename__ = "b"
+            id = Column(Integer, Identity(), primary_key=True)
+            a_id = Column(ForeignKey("a.id"))
+            boolean = query_expression()
+            d1 = Column(String(30))
+            d2 = Column(String(30))
+            d3 = Column(String(30))
+            d4 = Column(String(30))
+            d5 = Column(String(30))
+            d6 = Column(String(30))
+            d7 = Column(String(30))
+
+    @classmethod
+    def insert_data(cls, connection):
+        A, B = cls.classes("A", "B")
+
+        with Session(connection) as s:
+            s.add(
+                A(
+                    bs=[
+                        B(
+                            d1="x",
+                            d2="x",
+                            d3="x",
+                            d4="x",
+                            d5="x",
+                            d6="x",
+                            d7="x",
+                        )
+                    ]
+                )
+            )
+            s.commit()
+
+    def test_from_opt_no_cache(self):
+        A, B = self.classes("A", "B")
+
+        @profiling.function_call_count(warmup=2)
+        def go():
+            with Session(
+                testing.db.execution_options(compiled_cache=None)
+            ) as sess:
+                _ = sess.execute(
+                    select(A).options(
+                        selectinload(A.bs).options(
+                            with_expression(
+                                B.boolean,
+                                and_(
+                                    B.d1 == "x",
+                                    B.d2 == "x",
+                                    B.d3 == "x",
+                                    B.d4 == "x",
+                                    B.d5 == "x",
+                                    B.d6 == "x",
+                                    B.d7 == "x",
+                                ),
+                            )
+                        )
+                    )
+                ).scalars()
+
+        go()
+
+    def test_from_opt_after_cache(self):
+        A, B = self.classes("A", "B")
+
+        @profiling.function_call_count(warmup=2)
+        def go():
+            with Session(testing.db) as sess:
+                _ = sess.execute(
+                    select(A).options(
+                        selectinload(A.bs).options(
+                            with_expression(
+                                B.boolean,
+                                and_(
+                                    B.d1 == literal_column("'x'"),
+                                    B.d2 == "x",
+                                    B.d3 == literal_column("'x'"),
+                                    B.d4 == "x",
+                                    B.d5 == literal_column("'x'"),
+                                    B.d6 == "x",
+                                    B.d7 == literal_column("'x'"),
+                                ),
+                            )
+                        )
+                    )
+                ).scalars()
+
+        go()
index d943f418ff6691570e797b90acbde8db28448b13..d8226f4a8944ae3fe115f1cd5a83ce3fc5933365 100644 (file)
@@ -144,147 +144,188 @@ test.aaa_profiling.test_misc.EnumTest.test_create_enum_from_pep_435_w_expensive_
 # TEST: test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_bundle_w_annotation
 
 test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_bundle_w_annotation x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 55930
-test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_bundle_w_annotation x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 65740
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_bundle_w_annotation x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 65640
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_bundle_w_annotation x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 51230
 
 # TEST: test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_bundle_wo_annotation
 
 test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_bundle_wo_annotation x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 54230
-test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_bundle_wo_annotation x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 64040
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_bundle_wo_annotation x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 63940
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_bundle_wo_annotation x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 49530
 
 # TEST: test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_entity_w_annotations
 
 test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_entity_w_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 58530
-test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_entity_w_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 66440
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_entity_w_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 66240
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_entity_w_annotations x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 54730
 
 # TEST: test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_entity_wo_annotations
 
 test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_entity_wo_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 57530
-test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_entity_wo_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 65440
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_entity_wo_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 65240
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_entity_wo_annotations x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 53730
 
 # TEST: test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle
 
 test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 49130
-test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 51940
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 51840
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 46030
 
 # TEST: test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle_w_annotations
 
 test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle_w_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 52830
-test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle_w_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 60140
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle_w_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 60040
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle_w_annotations x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 49130
 
 # TEST: test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle_wo_annotations
 
 test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle_wo_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 51830
-test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle_wo_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 59140
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle_wo_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 59040
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_bundle_wo_annotations x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 48130
 
 # TEST: test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_entity_w_annotations
 
 test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_entity_w_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 37705
 test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_entity_w_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 40805
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_entity_w_annotations x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 34505
 
 # TEST: test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_entity_wo_annotations
 
 test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_entity_wo_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 36705
 test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_entity_wo_annotations x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 39805
+test.aaa_profiling.test_orm.AnnotatedOverheadTest.test_no_entity_wo_annotations x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 33505
 
 # TEST: test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set
 
 test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 3599
 test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 3599
+test.aaa_profiling.test_orm.AttributeOverheadTest.test_attribute_set x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 3598
 
 # TEST: test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove
 
 test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 5527
 test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 5527
+test.aaa_profiling.test_orm.AttributeOverheadTest.test_collection_append_remove x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 5526
 
 # TEST: test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_key_bound_branching
 
 test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_key_bound_branching x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 128
 test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_key_bound_branching x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 128
+test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_key_bound_branching x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 124
 
 # TEST: test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_unbound_branching
 
 test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_unbound_branching x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 128
 test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_unbound_branching x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 128
+test.aaa_profiling.test_orm.BranchedOptionTest.test_query_opts_unbound_branching x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 124
 
 # TEST: test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline
 
-test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 15359
-test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 24383
+test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 15360
+test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 24378
+test.aaa_profiling.test_orm.DeferOptionsTest.test_baseline x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 15325
 
 # TEST: test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols
 
-test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 21437
-test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 24461
+test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 21420
+test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 24444
+test.aaa_profiling.test_orm.DeferOptionsTest.test_defer_many_cols x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 21384
 
 # TEST: test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased
 
-test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 10654
-test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 11054
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 10804
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 11204
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 10754
 
 # TEST: test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased_select_join
 
 test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased_select_join x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 1154
 test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased_select_join x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 1154
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_aliased_select_join x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 1154
 
 # TEST: test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain
 
 test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 4304
 test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 4604
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_b_plain x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 4304
 
 # TEST: test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d
 
-test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 96282
-test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 109782
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 98632
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 112132
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 95532
 
 # TEST: test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d_aliased
 
-test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d_aliased x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 93732
-test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d_aliased x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 107432
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d_aliased x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 96082
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d_aliased x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 109782
+test.aaa_profiling.test_orm.JoinConditionTest.test_a_to_d_aliased x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 92982
 
 # TEST: test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results_integrated
 
-test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results_integrated x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 26339,1019,96653
-test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results_integrated x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 27438,1228,117553
+test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results_integrated x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 27016,1006,95353
+test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results_integrated x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 28168,1215,116253
+test.aaa_profiling.test_orm.JoinedEagerLoadTest.test_fetch_results_integrated x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 26604,974,92153
 
 # TEST: test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_identity
 
 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_identity x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 23981
 test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_identity x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 23981
+test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_identity x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 22982
 
 # TEST: test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity
 
-test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 113158
-test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 123916
+test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 113225
+test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 123983
+test.aaa_profiling.test_orm.LoadManyToOneFromIdentityTest.test_many_to_one_load_no_identity x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 108201
 
 # TEST: test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks
 
-test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 21189
-test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 22709
+test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 21197
+test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 22705
+test.aaa_profiling.test_orm.MergeBackrefsTest.test_merge_pending_with_all_pks x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 20478
 
 # TEST: test.aaa_profiling.test_orm.MergeTest.test_merge_load
 
-test.aaa_profiling.test_orm.MergeTest.test_merge_load x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 1480
-test.aaa_profiling.test_orm.MergeTest.test_merge_load x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 1583
+test.aaa_profiling.test_orm.MergeTest.test_merge_load x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 1481
+test.aaa_profiling.test_orm.MergeTest.test_merge_load x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 1581
+test.aaa_profiling.test_orm.MergeTest.test_merge_load x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 1412
 
 # TEST: test.aaa_profiling.test_orm.MergeTest.test_merge_no_load
 
 test.aaa_profiling.test_orm.MergeTest.test_merge_no_load x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 108,20
 test.aaa_profiling.test_orm.MergeTest.test_merge_no_load x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 108,20
+test.aaa_profiling.test_orm.MergeTest.test_merge_no_load x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 108,20
 
 # TEST: test.aaa_profiling.test_orm.QueryTest.test_query_cols
 
-test.aaa_profiling.test_orm.QueryTest.test_query_cols x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 6696
-test.aaa_profiling.test_orm.QueryTest.test_query_cols x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 7456
+test.aaa_profiling.test_orm.QueryTest.test_query_cols x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 6706
+test.aaa_profiling.test_orm.QueryTest.test_query_cols x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 7436
+test.aaa_profiling.test_orm.QueryTest.test_query_cols x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 6316
 
 # TEST: test.aaa_profiling.test_orm.SelectInEagerLoadTest.test_round_trip_results
 
-test.aaa_profiling.test_orm.SelectInEagerLoadTest.test_round_trip_results x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 277405
-test.aaa_profiling.test_orm.SelectInEagerLoadTest.test_round_trip_results x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 298505
+test.aaa_profiling.test_orm.SelectInEagerLoadTest.test_round_trip_results x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 277005
+test.aaa_profiling.test_orm.SelectInEagerLoadTest.test_round_trip_results x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 297305
+test.aaa_profiling.test_orm.SelectInEagerLoadTest.test_round_trip_results x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 263005
 
 # TEST: test.aaa_profiling.test_orm.SessionTest.test_expire_lots
 
 test.aaa_profiling.test_orm.SessionTest.test_expire_lots x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 1212
 test.aaa_profiling.test_orm.SessionTest.test_expire_lots x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 1212
+test.aaa_profiling.test_orm.SessionTest.test_expire_lots x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 1098
+
+# TEST: test.aaa_profiling.test_orm.WithExpresionLoaderOptTest.test_from_opt_after_cache
+
+test.aaa_profiling.test_orm.WithExpresionLoaderOptTest.test_from_opt_after_cache x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 1418
+test.aaa_profiling.test_orm.WithExpresionLoaderOptTest.test_from_opt_after_cache x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 1504
+test.aaa_profiling.test_orm.WithExpresionLoaderOptTest.test_from_opt_after_cache x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 1399
+
+# TEST: test.aaa_profiling.test_orm.WithExpresionLoaderOptTest.test_from_opt_no_cache
+
+test.aaa_profiling.test_orm.WithExpresionLoaderOptTest.test_from_opt_no_cache x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_cextensions 1859
+test.aaa_profiling.test_orm.WithExpresionLoaderOptTest.test_from_opt_no_cache x86_64_linux_cpython_3.11_sqlite_pysqlite_dbapiunicode_nocextensions 1880
+test.aaa_profiling.test_orm.WithExpresionLoaderOptTest.test_from_opt_no_cache x86_64_linux_cpython_3.12_sqlite_pysqlite_dbapiunicode_cextensions 1830
 
 # TEST: test.aaa_profiling.test_pool.QueuePoolTest.test_first_connect
 
index b2be90f60cdf621a2302001a4a899387f75dc43a..746058c679e6c4810f90e0c03925f09e9998f06a 100644 (file)
@@ -35,6 +35,7 @@ from sqlalchemy.schema import Sequence
 from sqlalchemy.sql import bindparam
 from sqlalchemy.sql import ColumnElement
 from sqlalchemy.sql import dml
+from sqlalchemy.sql import elements
 from sqlalchemy.sql import False_
 from sqlalchemy.sql import func
 from sqlalchemy.sql import operators
@@ -1368,7 +1369,7 @@ class CompareAndCopyTest(CoreFixtures, fixtures.TestBase):
                 "__init__" in cls.__dict__
                 or issubclass(cls, AliasedReturnsRows)
             )
-            and not issubclass(cls, (Annotated))
+            and not issubclass(cls, (Annotated, elements._OverrideBinds))
             and cls.__module__.startswith("sqlalchemy.")
             and "orm" not in cls.__module__
             and "compiler" not in cls.__module__