]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Ensure ClauseAdapter treats FunctionElement as a ColumnElement
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 18 Mar 2021 23:34:32 +0000 (19:34 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Fri, 19 Mar 2021 01:47:46 +0000 (21:47 -0400)
Fixed regression where use of an unnamed SQL expression such as a SQL
function would raise a column targeting error if the query itself were
using joinedload for an entity and was also being wrapped in a subquery by
the joinedload eager loading process.

Fixes: #6086
Change-Id: I22cf4d6974685267c4f903bd7639be8271c6c1ef

doc/build/changelog/unreleased_14/6086.rst [new file with mode: 0644]
lib/sqlalchemy/orm/context.py
lib/sqlalchemy/sql/util.py
test/orm/test_eager_relations.py
test/sql/test_external_traversal.py

diff --git a/doc/build/changelog/unreleased_14/6086.rst b/doc/build/changelog/unreleased_14/6086.rst
new file mode 100644 (file)
index 0000000..5510883
--- /dev/null
@@ -0,0 +1,9 @@
+.. change::
+    :tags: bug, regression, orm
+    :tickets: 6086
+
+    Fixed regression where use of an unnamed SQL expression such as a SQL
+    function would raise a column targeting error if the query itself were
+    using joinedload for an entity and was also being wrapped in a subquery by
+    the joinedload eager loading process.
+
index 9cfa0fdc6d7125deceb42fea00c73646f7362e37..e440ee16146ca35040279e09035fe5b06b89cf1a 100644 (file)
@@ -2707,7 +2707,9 @@ class _ORMColumnEntity(_ColumnEntity):
             compile_state._entities.append(self)
 
         compile_state._has_orm_entities = True
+
         self.column = column
+
         self._fetch_column = self._row_processor = None
 
         self._extra_entities = (self.expr, self.column)
index 5f35de5f002add73fb2c8d4fcf9ed5356a9e2036..4300d8a298b65c2f9796e5602d3d6251056c602f 100644 (file)
@@ -843,8 +843,13 @@ class ClauseAdapter(visitors.ReplacingExternalTraversal):
             newcol = self.selectable.exported_columns.get(col.name)
         return newcol
 
+    @util.preload_module("sqlalchemy.sql.functions")
     def replace(self, col):
-        if isinstance(col, FromClause):
+        functions = util.preloaded.sql_functions
+
+        if isinstance(col, FromClause) and not isinstance(
+            col, functions.FunctionElement
+        ):
             if self.selectable.is_derived_from(col):
                 return self.selectable
             elif isinstance(col, Alias) and isinstance(
index f62d3af3314cf82b433574d6112a8a62c39f46d8..289f8e8b1e7e212be2c98fa371f0d42c1351c328 100644 (file)
@@ -488,6 +488,37 @@ class EagerTest(_fixtures.FixtureTest, testing.AssertsCompiledSQL):
 
         self.assert_sql_count(testing.db, go, 1)
 
+    def test_aliased_stmt_includes_unnamed_fn(self):
+        User, Address = self.classes("User", "Address")
+        users, addresses = self.tables("users", "addresses")
+        mapper(
+            User,
+            users,
+            properties={"addresses": relationship(Address, lazy="joined")},
+        )
+        mapper(Address, addresses)
+
+        s = fixture_session()
+
+        # issue #6086
+        # statement wrapped in a subquery by limit() and group_by()
+        # func.count() is unlabeled (in 1.3 the _ColumnEntity would label it,
+        # in the ORM layer,  hence there was no problem here).
+        # the _ColumnEntity needs to adapt func.count(User.id) to the anon
+        # count_1 label on the outside, corresponding_column can do it.
+        # but ClauseAdapter has to treat the FunctionElement as a ColumnElement
+        # whereas previously it was treating it as a FromClause (and
+        # FunctionElement should really not even be a FromClause but there's
+        # legacy baggage on that)
+        q = (
+            s.query(User, func.count(User.id))
+            .order_by(User.id)
+            .group_by(User.id)
+            .limit(1)
+        )
+
+        eq_(q.first(), (User(id=7), 1))
+
     def test_options_pathing(self):
         (
             users,
index bc6013791306404a2702219350bb05895fcd1a62..b7e58dad9e82a642ca1bd635da6ff82110b49601 100644 (file)
@@ -1338,6 +1338,40 @@ class ClauseAdapterTest(fixtures.TestBase, AssertsCompiledSQL):
             "AS anon_1 FROM table1 AS t1alias",
         )
 
+    def test_adapt_select_w_unlabeled_fn(self):
+
+        expr = func.count(t1.c.col1)
+        stmt = select(t1, expr)
+
+        self.assert_compile(
+            stmt,
+            "SELECT table1.col1, table1.col2, table1.col3, "
+            "count(table1.col1) AS count_1 FROM table1",
+        )
+
+        stmt2 = select(stmt.subquery())
+
+        self.assert_compile(
+            stmt2,
+            "SELECT anon_1.col1, anon_1.col2, anon_1.col3, anon_1.count_1 "
+            "FROM (SELECT table1.col1 AS col1, table1.col2 AS col2, "
+            "table1.col3 AS col3, count(table1.col1) AS count_1 "
+            "FROM table1) AS anon_1",
+        )
+
+        is_(
+            stmt2.selected_columns[3],
+            stmt2.selected_columns.corresponding_column(expr),
+        )
+
+        is_(
+            sql_util.ClauseAdapter(stmt2).replace(expr),
+            stmt2.selected_columns[3],
+        )
+
+        column_adapter = sql_util.ColumnAdapter(stmt2)
+        is_(column_adapter.columns[expr], stmt2.selected_columns[3])
+
     def test_correlate_except_on_clone(self):
         # test [ticket:4537]'s issue