]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
dont pull filter_by from from_obj, entity is hopefully sufficient
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 26 Oct 2021 15:28:42 +0000 (11:28 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 26 Oct 2021 17:28:45 +0000 (13:28 -0400)
Fixed 1.4 regression where :meth:`_orm.Query.filter_by` would not function
correctly on a :class:`_orm.Query` that was produced from
:meth:`_orm.Query.union`, :meth:`_orm.Query.from_self` or similar.

Fixes: #7239
Change-Id: I3a0c3fd71180b1bfb6bf855f436a29c729664082

doc/build/changelog/unreleased_14/7239.rst [new file with mode: 0644]
lib/sqlalchemy/orm/query.py
test/orm/test_deprecations.py
test/orm/test_query.py

diff --git a/doc/build/changelog/unreleased_14/7239.rst b/doc/build/changelog/unreleased_14/7239.rst
new file mode 100644 (file)
index 0000000..14ef191
--- /dev/null
@@ -0,0 +1,7 @@
+.. change::
+    :tags: bug, orm, regression
+    :tickets: 7239
+
+    Fixed 1.4 regression where :meth:`_orm.Query.filter_by` would not function
+    correctly on a :class:`_orm.Query` that was produced from
+    :meth:`_orm.Query.union`, :meth:`_orm.Query.from_self` or similar.
index fcb9dae57ad0227bf47eedcba731745062a02c2c..d48e34923ad6f162dd8dc20896710d952e19e900 100644 (file)
@@ -1707,12 +1707,42 @@ class Query(
             return None
 
     def _filter_by_zero(self):
+        """for the filter_by() method, return the target entity for which
+        we will attempt to derive an expression from based on string name.
+
+        """
         if self._legacy_setup_joins:
             _last_joined_entity = self._last_joined_entity
             if _last_joined_entity is not None:
                 return _last_joined_entity
 
-        if self._from_obj:
+        # discussion related to #7239
+        # special check determines if we should try to derive attributes
+        # for filter_by() from the "from object", i.e., if the user
+        # called query.select_from(some selectable).filter_by(some_attr=value).
+        # We don't want to do that in the case that methods like
+        # from_self(), select_entity_from(), or a set op like union() were
+        # called; while these methods also place a
+        # selectable in the _from_obj collection, they also set up
+        # the _set_base_alias boolean which turns on the whole "adapt the
+        # entity to this selectable" thing, meaning the query still continues
+        # to construct itself in terms of the lead entity that was passed
+        # to query(), e.g. query(User).from_self() is still in terms of User,
+        # and not the subquery that from_self() created.   This feature of
+        # "implicitly adapt all occurrences of entity X to some arbitrary
+        # subquery" is the main thing I am trying to do away with in 2.0 as
+        # users should now used aliased() for that, but I can't entirely get
+        # rid of it due to query.union() and other set ops relying upon it.
+        #
+        # compare this to the base Select()._filter_by_zero() which can
+        # just return self._from_obj[0] if present, because there is no
+        # "_set_base_alias" feature.
+        #
+        # IOW, this conditional essentially detects if
+        # "select_from(some_selectable)" has been called, as opposed to
+        # "select_entity_from()", "from_self()"
+        # or "union() / some_set_op()".
+        if self._from_obj and not self._compile_options._set_base_alias:
             return self._from_obj[0]
 
         return self._raw_columns[0]
index b868c63fe1a6e29787c9251f5df560aaeb2c240f..c66259e4789e8f2cf81f2153b24c5b3932f26549 100644 (file)
@@ -10,6 +10,7 @@ from sqlalchemy import func
 from sqlalchemy import inspect
 from sqlalchemy import Integer
 from sqlalchemy import join
+from sqlalchemy import LABEL_STYLE_TABLENAME_PLUS_COL
 from sqlalchemy import lateral
 from sqlalchemy import literal_column
 from sqlalchemy import MetaData
@@ -79,6 +80,7 @@ from sqlalchemy.testing.mock import call
 from sqlalchemy.testing.mock import Mock
 from sqlalchemy.testing.schema import Column
 from sqlalchemy.testing.schema import Table
+from sqlalchemy.testing.util import resolve_lambda
 from sqlalchemy.util import pickle
 from . import _fixtures
 from .inheritance import _poly_fixtures
@@ -1605,6 +1607,24 @@ class FromSelfTest(QueryTest, AssertsCompiledSQL):
             {},
         )
 
+    def test_basic_filter_by(self):
+        """test #7239"""
+
+        User = self.classes.User
+
+        s = fixture_session()
+
+        with self._from_self_deprecated():
+            q = s.query(User).from_self()
+
+        self.assert_compile(
+            q.filter_by(id=5),
+            "SELECT anon_1.users_id AS anon_1_users_id, anon_1.users_name "
+            "AS anon_1_users_name FROM (SELECT users.id AS users_id, "
+            "users.name AS users_name FROM users) AS anon_1 "
+            "WHERE anon_1.users_id = :id_1",
+        )
+
     def test_columns_augmented_distinct_on(self):
         User, Address = self.classes.User, self.classes.Address
 
@@ -7328,6 +7348,52 @@ class SelectFromTest(QueryTest, AssertsCompiledSQL):
                 "AS anon_1",
             )
 
+    @testing.combinations(
+        (
+            lambda users: users.select().where(users.c.id.in_([7, 8])),
+            "SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name "
+            "FROM (SELECT users.id AS id, users.name AS name "
+            "FROM users WHERE users.id IN ([POSTCOMPILE_id_1])) AS anon_1 "
+            "WHERE anon_1.name = :name_1",
+        ),
+        (
+            lambda users: users.select()
+            .where(users.c.id.in_([7, 8]))
+            .set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL),
+            "SELECT anon_1.users_id AS anon_1_users_id, anon_1.users_name "
+            "AS anon_1_users_name FROM (SELECT users.id AS users_id, "
+            "users.name AS users_name FROM users "
+            "WHERE users.id IN ([POSTCOMPILE_id_1])) AS anon_1 "
+            "WHERE anon_1.users_name = :name_1",
+        ),
+        (
+            lambda User, sess: sess.query(User).where(User.id.in_([7, 8])),
+            "SELECT anon_1.id AS anon_1_id, anon_1.name AS anon_1_name "
+            "FROM (SELECT users.id AS id, users.name AS name "
+            "FROM users WHERE users.id IN ([POSTCOMPILE_id_1])) AS anon_1 "
+            "WHERE anon_1.name = :name_1",
+        ),
+    )
+    def test_filter_by(self, query_fn, expected):
+        """test #7239"""
+
+        User = self.classes.User
+        sess = fixture_session()
+
+        User, users = self.classes.User, self.tables.users
+
+        self.mapper_registry.map_imperatively(User, users)
+
+        sel = resolve_lambda(query_fn, User=User, users=users, sess=sess)
+
+        sess = fixture_session()
+
+        with assertions.expect_deprecated_20(sef_dep):
+            q = sess.query(User).select_entity_from(sel.subquery())
+
+        self.assert_compile(q.filter_by(name="ed"), expected)
+        eq_(q.filter_by(name="ed").all(), [User(name="ed")])
+
     def test_join_no_order_by(self):
         User, users = self.classes.User, self.tables.users
 
index 2320124cddcc42bb8c70f677fb87b089c15f8c0d..1971964b1338b5b28784bdf895a60c6b3f83d7c2 100644 (file)
@@ -3619,6 +3619,47 @@ class FilterTest(QueryTest, AssertsCompiledSQL):
             "WHERE users.name = :name_1",
         )
 
+    def test_filter_by_against_union_legacy(self):
+        """test #7239"""
+
+        User = self.classes.User
+        sess = fixture_session()
+
+        q = (
+            sess.query(User)
+            .filter(User.id == 2)
+            .union(sess.query(User).filter(User.id == 5))
+        )
+
+        self.assert_compile(
+            q.filter_by(id=5),
+            "SELECT anon_1.users_id AS anon_1_users_id, anon_1.users_name "
+            "AS anon_1_users_name FROM (SELECT users.id AS users_id, "
+            "users.name AS users_name FROM users WHERE users.id = :id_1 "
+            "UNION SELECT users.id AS users_id, users.name AS users_name "
+            "FROM users WHERE users.id = :id_2) AS anon_1 "
+            "WHERE anon_1.users_id = :id_3",
+        )
+
+    def test_filter_by_against_union_newstyle(self):
+        """test #7239"""
+
+        User = self.classes.User
+
+        u = union(
+            select(User).filter(User.id == 2),
+            select(User).filter(User.id == 5),
+        )
+        ua = aliased(User, u.subquery())
+        stmt = select(ua)
+        self.assert_compile(
+            stmt.filter_by(id=5),
+            "SELECT anon_1.id, anon_1.name FROM (SELECT users.id AS id, "
+            "users.name AS name FROM users WHERE users.id = :id_1 "
+            "UNION SELECT users.id AS id, users.name AS name FROM users "
+            "WHERE users.id = :id_2) AS anon_1 WHERE anon_1.id = :id_3",
+        )
+
     def test_filter_by_against_cast(self):
         """test #6414"""
         User = self.classes.User