]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
turn off eager configure_mappers() outside of Query, Load
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 16 Mar 2021 21:59:44 +0000 (17:59 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 16 Mar 2021 21:59:44 +0000 (17:59 -0400)
Fixed regression where producing a Core expression construct such as
:func:`_sql.select` using ORM entities would eagerly configure the mappers,
in an effort to maintain compatibility with the :class:`_orm.Query` object
which necessarily does this to support many backref-related legacy cases.
However, core :func:`_sql.select` constructs are also used in mapper
configurations and such, and to that degree this eager configuration is
more of an inconvenience, so eager configure has been disabled for the
:func:`_sql.select` and other Core constructs in the absence of ORM loading
types of functions such as :class:`_orm.Load`.

The change maintains the behavior of :class:`_orm.Query` so that backwards
compatibility is maintained. However, when using a :func:`_sql.select` in
conjunction with ORM entities, a "backref" that isn't explicitly placed on
one of the classes until mapper configure time won't be available unless
:func:`_orm.configure_mappers` or the newer :func:`_orm.registry.configure`
has been called elsewhere. Prefer using
:paramref:`_orm.relationship.back_populates` for more explicit relationship
configuration which does not have the eager configure requirement.

Fixes: #6066
Change-Id: I7a953ddcc189471fbac63c97c51ab8956f64012e

doc/build/changelog/unreleased_14/6066.rst [new file with mode: 0644]
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/strategy_options.py
lib/sqlalchemy/sql/coercions.py
test/orm/test_events.py
test/orm/test_mapper.py

diff --git a/doc/build/changelog/unreleased_14/6066.rst b/doc/build/changelog/unreleased_14/6066.rst
new file mode 100644 (file)
index 0000000..e2eba5a
--- /dev/null
@@ -0,0 +1,23 @@
+.. change::
+    :tags: bug, orm, regression
+    :tickets: 6066
+
+    Fixed regression where producing a Core expression construct such as
+    :func:`_sql.select` using ORM entities would eagerly configure the mappers,
+    in an effort to maintain compatibility with the :class:`_orm.Query` object
+    which necessarily does this to support many backref-related legacy cases.
+    However, core :func:`_sql.select` constructs are also used in mapper
+    configurations and such, and to that degree this eager configuration is
+    more of an inconvenience, so eager configure has been disabled for the
+    :func:`_sql.select` and other Core constructs in the absence of ORM loading
+    types of functions such as :class:`_orm.Load`.
+
+    The change maintains the behavior of :class:`_orm.Query` so that backwards
+    compatibility is maintained. However, when using a :func:`_sql.select` in
+    conjunction with ORM entities, a "backref" that isn't explicitly placed on
+    one of the classes until mapper configure time won't be available unless
+    :func:`_orm.configure_mappers` or the newer :func:`_orm.registry.configure`
+    has been called elsewhere. Prefer using
+    :paramref:`_orm.relationship.back_populates` for more explicit relationship
+    configuration which does not have the eager configure requirement.
+
index 4e2b4cdebbcff8e6878ef4e8958e76486c5cfa37..bbad6b05a24ba046d2627caffca02441d263e49b 100644 (file)
@@ -179,7 +179,10 @@ class Query(
     def _set_entities(self, entities):
         self._raw_columns = [
             coercions.expect(
-                roles.ColumnsClauseRole, ent, apply_propagate_attrs=self
+                roles.ColumnsClauseRole,
+                ent,
+                apply_propagate_attrs=self,
+                post_inspect=True,
             )
             for ent in util.to_list(entities)
         ]
@@ -1415,7 +1418,10 @@ class Query(
 
         self._raw_columns.extend(
             coercions.expect(
-                roles.ColumnsClauseRole, c, apply_propagate_attrs=self
+                roles.ColumnsClauseRole,
+                c,
+                apply_propagate_attrs=self,
+                post_inspect=True,
             )
             for c in column
         )
@@ -3217,7 +3223,10 @@ class FromStatement(GroupedElement, SelectBase, Executable):
     def __init__(self, entities, element):
         self._raw_columns = [
             coercions.expect(
-                roles.ColumnsClauseRole, ent, apply_propagate_attrs=self
+                roles.ColumnsClauseRole,
+                ent,
+                apply_propagate_attrs=self,
+                post_inspect=True,
             )
             for ent in util.to_list(entities)
         ]
index f24f42a6b37c37ee4ea9b6ee1226e2f7b9d56575..932c4a37d0ff0952cf63e2346fb4e02d1f7ab235 100644 (file)
@@ -89,6 +89,8 @@ class Load(Generative, LoaderOption):
 
     def __init__(self, entity):
         insp = inspect(entity)
+        insp._post_inspect
+
         self.path = insp._path_registry
         # note that this .context is shared among all descendant
         # Load objects
index 8c7963205f3933e63d5c123ef6f387d74d1ccc1e..76ba7e2146933c5c9639a112774e49979becdc24 100644 (file)
@@ -109,7 +109,14 @@ def _expression_collection_was_a_list(attrname, fnname, args):
         return args
 
 
-def expect(role, element, apply_propagate_attrs=None, argname=None, **kw):
+def expect(
+    role,
+    element,
+    apply_propagate_attrs=None,
+    argname=None,
+    post_inspect=False,
+    **kw
+):
     if (
         role.allows_lambda
         # note callable() will not invoke a __getattr__() method, whereas
@@ -157,7 +164,8 @@ def expect(role, element, apply_propagate_attrs=None, argname=None, **kw):
                 if impl._use_inspection:
                     insp = inspection.inspect(element, raiseerr=False)
                     if insp is not None:
-                        insp._post_inspect
+                        if post_inspect:
+                            insp._post_inspect
                         try:
                             resolved = insp.__clause_element__()
                         except AttributeError:
index 6ba94096d13d432600668ac1c5df398f6c7e401f..cd8adf8e35ebcc1df7b40f3ae248a2f2fbb701e8 100644 (file)
@@ -999,8 +999,7 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
         event.listen(mapper, "before_configured", m1)
         event.listen(mapper, "after_configured", m2)
 
-        s = fixture_session()
-        s.query(User)
+        inspect(User)._post_inspect
 
         eq_(m1.mock_calls, [call()])
         eq_(m2.mock_calls, [call()])
@@ -1132,8 +1131,7 @@ class MapperEventsTest(_RemoveListeners, _fixtures.FixtureTest):
         # fails by default because Mammal needs to be configured, and cannot
         # be:
         def probe():
-            s = fixture_session()
-            s.query(User)
+            inspect(User)._post_inspect
 
         if create_dependency:
             assert_raises(sa.exc.InvalidRequestError, probe)
index ba4cf26373eb0f15ab715f51a44400ad7a770d2b..e4c89c7e8a9c42924e1c9abf20bdc0028550b36a 100644 (file)
@@ -21,6 +21,8 @@ from sqlalchemy.orm import composite
 from sqlalchemy.orm import configure_mappers
 from sqlalchemy.orm import deferred
 from sqlalchemy.orm import dynamic_loader
+from sqlalchemy.orm import Load
+from sqlalchemy.orm import load_only
 from sqlalchemy.orm import mapper
 from sqlalchemy.orm import reconstructor
 from sqlalchemy.orm import registry
@@ -2998,3 +3000,136 @@ class RegistryConfigDisposeTest(fixtures.TestBase):
                 "pass cascade=True to clear these also",
             ):
                 reg3.dispose()
+
+
+class ConfigureOrNotConfigureTest(_fixtures.FixtureTest, AssertsCompiledSQL):
+    __dialect__ = "default"
+
+    @testing.combinations((True,), (False,))
+    def test_no_mapper_configure_w_selects_etc(self, use_legacy_query):
+        Address, addresses, users, User = (
+            self.classes.Address,
+            self.tables.addresses,
+            self.tables.users,
+            self.classes.User,
+        )
+
+        am = self.mapper(Address, addresses)
+
+        um = self.mapper(
+            User,
+            users,
+            properties={
+                "address_count": column_property(
+                    select(Address)
+                    .where(Address.id == users.c.id)
+                    .correlate_except(Address)
+                    .scalar_subquery()
+                )
+            },
+        )
+
+        is_false(am.configured)
+        is_false(um.configured)
+
+        if use_legacy_query:
+            stmt = Session().query(User).filter(User.name == "ed")
+            self.assert_compile(
+                stmt,
+                "SELECT (SELECT addresses.id, addresses.user_id, "
+                "addresses.email_address FROM addresses "
+                "WHERE addresses.id = users.id) AS anon_1, "
+                "users.id AS users_id, users.name AS users_name "
+                "FROM users WHERE users.name = :name_1",
+            )
+        else:
+            stmt = select(User).where(User.name == "ed")
+
+            self.assert_compile(
+                stmt,
+                "SELECT (SELECT addresses.id, addresses.user_id, "
+                "addresses.email_address FROM addresses "
+                "WHERE addresses.id = users.id) AS anon_1, "
+                "users.id, users.name "
+                "FROM users WHERE users.name = :name_1",
+            )
+
+        is_true(am.configured)
+        is_true(um.configured)
+
+    @testing.combinations((True,), (False,))
+    def test_load_options(self, use_bound):
+        User = self.classes.User
+
+        users = self.tables.users
+
+        um = mapper(User, users)
+
+        if use_bound:
+            stmt = select(User).options(
+                Load(User).load_only("name"),
+            )
+
+            is_true(um.configured)
+        else:
+            stmt = select(User).options(
+                load_only("name"),
+            )
+            is_false(um.configured)
+
+        self.assert_compile(
+            stmt,
+            "SELECT users.id, " "users.name " "FROM users",
+        )
+        is_true(um.configured)
+
+    @testing.combinations((True,), (False,))
+    def test_backrefs(self, use_legacy_query):
+        User, Address = self.classes("User", "Address")
+        users, addresses = self.tables("users", "addresses")
+
+        mapper(
+            User,
+            users,
+            properties={"addresses": relationship(Address, backref="user")},
+        )
+        am = mapper(Address, addresses)
+
+        if use_legacy_query:
+            s = Session()
+
+            # legacy, Query still forces configure
+            stmt = s.query(Address).join(Address.user)
+
+            is_true(am.configured)
+
+            self.assert_compile(
+                stmt,
+                "SELECT addresses.id AS addresses_id, "
+                "addresses.user_id AS addresses_user_id, "
+                "addresses.email_address AS addresses_email_address "
+                "FROM addresses JOIN users ON users.id = addresses.user_id",
+            )
+        else:
+            # new queries, they can't, because they are used in mapper
+            # config also.  backrefs that aren't explicit on the class
+            # are the only thing we can't do. we would need __getattr__
+            # to intercept this error.
+            with expect_raises_message(
+                AttributeError, "type object 'Address' has no attribute 'user'"
+            ):
+                stmt = select(Address).join(Address.user)
+
+            is_false(am.configured)
+
+            configure_mappers()
+
+            is_true(am.configured)
+
+            stmt = select(Address).join(Address.user)
+            self.assert_compile(
+                stmt,
+                "SELECT addresses.id, addresses.user_id, "
+                "addresses.email_address FROM addresses JOIN users "
+                "ON users.id = addresses.user_id",
+            )