]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Disallow AliasedReturnsRows from execution
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 6 Apr 2021 02:14:18 +0000 (22:14 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 6 Apr 2021 03:35:53 +0000 (23:35 -0400)
Executing a :class:`_sql.Subquery` using :meth:`_engine.Connection.execute`
is deprecated and will emit a deprecation warning; this use case was an
oversight that should have been removed from 1.4. The operation will now
execute the underlying :class:`_sql.Select` object directly for backwards
compatibility. Similarly, the :class:`_sql.CTE` class is also not
appropriate for execution. In 1.3, attempting to execute a CTE would result
in an invalid "blank" SQL statement being executed; since this use case was
not working it now raises :class:`_exc.ObjectNotExecutableError`.
Previously, 1.4 was attempting to execute the CTE as a statement however it
was working only erratically.

The change also breaks out StatementRole from ReturnsRowsRole, as these
roles should not be in the same lineage (some statements don't return
rows, the whole class of ReturnsRows that are from clauses are
not statements).    Consolidate StatementRole and
CoerceTextStatementRole as there's no usage difference between
these.   Simplify some old tests that were trying to make
sure that "execution options" didn't transmit from a cte/subquery
out to a select; as cte/subuqery() aren't executable in any case
the options are removed.

Fixes: #6204
Change-Id: I62613b7ab418afdd22c409eae75659e3f52fb65f

15 files changed:
doc/build/changelog/unreleased_14/6204.rst [new file with mode: 0644]
lib/sqlalchemy/orm/interfaces.py
lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/session.py
lib/sqlalchemy/sql/base.py
lib/sqlalchemy/sql/coercions.py
lib/sqlalchemy/sql/elements.py
lib/sqlalchemy/sql/lambdas.py
lib/sqlalchemy/sql/roles.py
lib/sqlalchemy/sql/selectable.py
lib/sqlalchemy/testing/suite/test_results.py
test/engine/test_execute.py
test/sql/test_compiler.py
test/sql/test_cte.py
test/sql/test_roles.py

diff --git a/doc/build/changelog/unreleased_14/6204.rst b/doc/build/changelog/unreleased_14/6204.rst
new file mode 100644 (file)
index 0000000..b6c518c
--- /dev/null
@@ -0,0 +1,14 @@
+.. change::
+    :tags: bug, sql
+    :tickets: 6204
+
+    Executing a :class:`_sql.Subquery` using :meth:`_engine.Connection.execute`
+    is deprecated and will emit a deprecation warning; this use case was an
+    oversight that should have been removed from 1.4. The operation will now
+    execute the underlying :class:`_sql.Select` object directly for backwards
+    compatibility. Similarly, the :class:`_sql.CTE` class is also not
+    appropriate for execution. In 1.3, attempting to execute a CTE would result
+    in an invalid "blank" SQL statement being executed; since this use case was
+    not working it now raises :class:`_exc.ObjectNotExecutableError`.
+    Previously, 1.4 was attempting to execute the CTE as a statement however it
+    was working only erratically.
index e2cc3699970d1c3607ce3a375467438058ea2070..6f77fd70656414d08f22c78747cc816671a51b62 100644 (file)
@@ -65,7 +65,7 @@ __all__ = (
 )
 
 
-class ORMStatementRole(roles.CoerceTextStatementRole):
+class ORMStatementRole(roles.StatementRole):
     _role_name = (
         "Executable SQL or text() construct, including ORM " "aware objects"
     )
index 7e2fde74972c4d30d9023fff904d2d8e0e8d718c..b94f9ac3715c045506e8d49b40e351f79d78e5e7 100644 (file)
@@ -3234,8 +3234,6 @@ class FromStatement(GroupedElement, SelectBase, Executable):
 
     _compile_state_factory = ORMFromStatementCompileState.create_for_statement
 
-    _is_future = True
-
     _for_update_arg = None
 
     _traverse_internals = [
index 340017adf1f507316df4f2457d0f19c6ab08a748..0562569bff23022cac58b79da9b29995fcf01475 100644 (file)
@@ -1584,7 +1584,7 @@ class Session(_SessionClassMethods):
 
 
         """
-        statement = coercions.expect(roles.CoerceTextStatementRole, statement)
+        statement = coercions.expect(roles.StatementRole, statement)
 
         if kw:
             util.warn_deprecated_20(
index ac9d6697027c5d83430a5a8b845d19c2a9b03176..81685dfe0c10dfc66d2ce668e07c325ae5f397f5 100644 (file)
@@ -754,7 +754,7 @@ class ExecutableOption(HasCopyInternals, HasCacheKey):
         return c
 
 
-class Executable(roles.CoerceTextStatementRole, Generative):
+class Executable(roles.StatementRole, Generative):
     """Mark a :class:`_expression.ClauseElement` as supporting execution.
 
     :class:`.Executable` is a superclass for all "statement" types
index 35ac1a5ba117512b0d95e60f825985b1f47cf2a9..c00262fd5ade634c505f0aa501dd62cc47d9cb36 100644 (file)
@@ -824,11 +824,7 @@ class ReturnsRowsImpl(RoleImpl):
     __slots__ = ()
 
 
-class StatementImpl(_NoTextCoercion, RoleImpl):
-    __slots__ = ()
-
-
-class CoerceTextStatementImpl(_CoerceLiterals, RoleImpl):
+class StatementImpl(_CoerceLiterals, RoleImpl):
     __slots__ = ()
 
     def _implicit_coercions(
@@ -837,7 +833,7 @@ class CoerceTextStatementImpl(_CoerceLiterals, RoleImpl):
         if resolved._is_lambda_element:
             return resolved
         else:
-            return super(CoerceTextStatementImpl, self)._implicit_coercions(
+            return super(StatementImpl, self)._implicit_coercions(
                 original_element, resolved, argname=argname, **kw
             )
 
index c4833830337c352e36bf2a138cd859a77f193204..dfa6c0f8f41cd9922b2720869a176777d6853572 100644 (file)
@@ -307,9 +307,9 @@ class ClauseElement(
         return d
 
     def _execute_on_connection(
-        self, connection, multiparams, params, execution_options
+        self, connection, multiparams, params, execution_options, _force=False
     ):
-        if self.supports_execution:
+        if _force or self.supports_execution:
             return connection._execute_clauseelement(
                 self, multiparams, params, execution_options
             )
index ebf576c8f2e48a58334c89ce1aa2918848002d04..06db8f95e395ec86988caabe2d090a831500b95e 100644 (file)
@@ -100,7 +100,7 @@ def lambda_stmt(
 
     return StatementLambdaElement(
         lmb,
-        roles.CoerceTextStatementRole,
+        roles.StatementRole,
         LambdaOptions(
             enable_tracking=enable_tracking,
             track_on=track_on,
@@ -155,9 +155,7 @@ class LambdaElement(elements.ClauseElement):
         self.tracker_key = (fn.__code__,)
         self.opts = opts
 
-        if apply_propagate_attrs is None and (
-            role is roles.CoerceTextStatementRole
-        ):
+        if apply_propagate_attrs is None and (role is roles.StatementRole):
             apply_propagate_attrs = self
 
         rec = self._retrieve_tracker_rec(fn, apply_propagate_attrs, opts)
@@ -492,10 +490,6 @@ class StatementLambdaElement(roles.AllowsLambdaRole, LambdaElement):
     def _effective_plugin_target(self):
         return self._rec.expected_expr._effective_plugin_target
 
-    @property
-    def _is_future(self):
-        return self._rec.expected_expr._is_future
-
     @property
     def _execution_options(self):
         return self._rec.expected_expr._execution_options
index 1c8276eb61b90443a6dc6b270c8beb56d1057728..7d64e83826ce67688a6c6090ed2760b9cf5d390d 100644 (file)
@@ -155,26 +155,20 @@ class AnonymizedFromClauseRole(StrictFromClauseRole):
         raise NotImplementedError()
 
 
-class CoerceTextStatementRole(SQLRole):
-    _role_name = "Executable SQL or text() construct"
+class ReturnsRowsRole(SQLRole):
+    _role_name = (
+        "Row returning expression such as a SELECT, a FROM clause, or an "
+        "INSERT/UPDATE/DELETE with RETURNING"
+    )
 
 
-class StatementRole(CoerceTextStatementRole):
+class StatementRole(SQLRole):
     _role_name = "Executable SQL or text() construct"
 
-    _is_future = False
-
     _propagate_attrs = util.immutabledict()
 
 
-class ReturnsRowsRole(StatementRole):
-    _role_name = (
-        "Row returning expression such as a SELECT, a FROM clause, or an "
-        "INSERT/UPDATE/DELETE with RETURNING"
-    )
-
-
-class SelectStatementRole(ReturnsRowsRole):
+class SelectStatementRole(StatementRole, ReturnsRowsRole):
     _role_name = "SELECT construct or equivalent text() construct"
 
     def subquery(self):
index a2e5780f8a50804a207542ec8dced55f1ee16784..f12744cfab5437aaaca41d281c73819b40b1562d 100644 (file)
@@ -1575,9 +1575,6 @@ class AliasedReturnsRows(NoInit, FromClause):
         self.element = coercions.expect(
             roles.ReturnsRowsRole, selectable, apply_propagate_attrs=self
         )
-        self.supports_execution = selectable.supports_execution
-        if self.supports_execution:
-            self._execution_options = selectable._execution_options
         self.element = selectable
         self._orig_name = name
         if name is None:
@@ -2338,6 +2335,23 @@ class Subquery(AliasedReturnsRows):
     def as_scalar(self):
         return self.element.set_label_style(LABEL_STYLE_NONE).scalar_subquery()
 
+    def _execute_on_connection(
+        self,
+        connection,
+        multiparams,
+        params,
+        execution_options,
+    ):
+        util.warn_deprecated(
+            "Executing a subquery object is deprecated and will raise "
+            "ObjectNotExecutableError in an upcoming release.  Please "
+            "execute the underlying select() statement directly.",
+            "1.4",
+        )
+        return self.element._execute_on_connection(
+            connection, multiparams, params, execution_options, _force=True
+        )
+
 
 class FromGrouping(GroupedElement, FromClause):
     """Represent a grouping of a FROM clause"""
@@ -4485,7 +4499,6 @@ class Select(
             ("_distinct", InternalTraversal.dp_boolean),
             ("_distinct_on", InternalTraversal.dp_clauseelement_tuple),
             ("_label_style", InternalTraversal.dp_plain_obj),
-            ("_is_future", InternalTraversal.dp_boolean),
         ]
         + HasPrefixes._has_prefixes_traverse_internals
         + HasSuffixes._has_suffixes_traverse_internals
index 6c2880ad48cb034557dd60d9d2b61a5ed01a6034..e8ad88f24ae78839169edf4f5528eca54357e2de 100644 (file)
@@ -333,14 +333,18 @@ class ServerSideCursorsTest(
 
     def test_aliases_and_ss(self):
         engine = self._fixture(False)
-        s1 = select(1).execution_options(stream_results=True).alias()
+        s1 = (
+            select(sql.literal_column("1").label("x"))
+            .execution_options(stream_results=True)
+            .subquery()
+        )
+
+        # options don't propagate out when subquery is used as a FROM clause
         with engine.begin() as conn:
-            result = conn.execute(s1)
-            assert self._is_server_side(result.cursor)
+            result = conn.execute(s1.select())
+            assert not self._is_server_side(result.cursor)
             result.close()
 
-        # s1's options shouldn't affect s2 when s2 is used as a
-        # from_obj.
         s2 = select(1).select_from(s1)
         with engine.begin() as conn:
             result = conn.execute(s2)
index 09219cfdbb75d8f32b9f1743780af50d4225dc38..0de5ed124354887b879b5fa165bc33c9578c4051 100644 (file)
@@ -42,6 +42,7 @@ from sqlalchemy.testing import is_false
 from sqlalchemy.testing import is_not
 from sqlalchemy.testing import is_true
 from sqlalchemy.testing import mock
+from sqlalchemy.testing.assertions import expect_deprecated
 from sqlalchemy.testing.assertsql import CompiledSQL
 from sqlalchemy.testing.mock import call
 from sqlalchemy.testing.mock import Mock
@@ -349,6 +350,8 @@ class ExecuteTest(fixtures.TablesTest):
             tsa.and_(True).compile(),
             column("foo"),
             column("foo").compile(),
+            select(1).cte(),
+            # select(1).subquery(),
             MetaData(),
             Integer(),
             tsa.Index(name="foo"),
@@ -362,6 +365,15 @@ class ExecuteTest(fixtures.TablesTest):
                     obj,
                 )
 
+    def test_subquery_exec_warning(self):
+        for obj in (select(1).alias(), select(1).subquery()):
+            with testing.db.connect() as conn:
+                with expect_deprecated(
+                    "Executing a subquery object is deprecated and will "
+                    "raise ObjectNotExecutableError"
+                ):
+                    eq_(conn.execute(obj).scalar(), 1)
+
     def test_stmt_exception_bytestring_raised(self):
         name = util.u("méil")
         users = self.tables.users
index b2d44343849896c426234d79756a9fd6ce6c2c9f..afa6aecf8a041e1780cf963e7d498ec4d2babbe6 100644 (file)
@@ -4550,20 +4550,20 @@ class ExecutionOptionsTest(fixtures.TestBase):
         eq_(compiled.execution_options, {"autocommit": True})
 
     def test_embedded_element_true_to_none(self):
-        stmt = table1.insert().cte()
+        stmt = table1.insert()
         eq_(stmt._execution_options, {"autocommit": True})
-        s2 = select(table1).select_from(stmt)
+        s2 = select(table1).select_from(stmt.cte())
         eq_(s2._execution_options, {})
 
         compiled = s2.compile()
         eq_(compiled.execution_options, {"autocommit": True})
 
     def test_embedded_element_true_to_false(self):
-        stmt = table1.insert().cte()
+        stmt = table1.insert()
         eq_(stmt._execution_options, {"autocommit": True})
         s2 = (
             select(table1)
-            .select_from(stmt)
+            .select_from(stmt.cte())
             .execution_options(autocommit=False)
         )
         eq_(s2._execution_options, {"autocommit": False})
index 92f3b215ff13716ef7239500c2f25bd19a28c401..fc2c9b40d755b02c9fb5edd52d72bc9ff8491340 100644 (file)
@@ -1150,7 +1150,7 @@ class CTETest(fixtures.TestBase, AssertsCompiledSQL):
         products = table("products", column("id"), column("price"))
 
         cte = products.select().cte("pd")
-        assert "autocommit" not in cte._execution_options
+        assert "autocommit" not in cte.select()._execution_options
 
         stmt = products.update().where(products.c.price == cte.c.price)
         eq_(stmt.compile().execution_options["autocommit"], True)
index e47a7e88973c45fa91cbcdb8f109db50b95ae1e5..9973110712be6de7bb34a471f28a6dd3c51eac9c 100644 (file)
@@ -209,24 +209,14 @@ class RoleTest(fixtures.TestBase):
         ):
             expect(roles.ExpressionElementRole, t.select().alias())
 
-    def test_statement_no_text_coercion(self):
-        assert_raises_message(
-            exc.ArgumentError,
-            r"Textual SQL expression 'select \* from table' should be "
-            r"explicitly declared",
-            expect,
-            roles.StatementRole,
-            "select * from table",
-        )
-
     def test_statement_text_coercion(self):
         with testing.expect_deprecated_20(
             "Using plain strings to indicate SQL statements"
         ):
             is_true(
-                expect(
-                    roles.CoerceTextStatementRole, "select * from table"
-                ).compare(text("select * from table"))
+                expect(roles.StatementRole, "select * from table").compare(
+                    text("select * from table")
+                )
             )
 
     def test_select_statement_no_text_coercion(self):
@@ -282,13 +272,11 @@ class RoleTest(fixtures.TestBase):
         )
 
     def test_statement_coercion_select(self):
-        is_true(
-            expect(roles.CoerceTextStatementRole, select(t)).compare(select(t))
-        )
+        is_true(expect(roles.StatementRole, select(t)).compare(select(t)))
 
     def test_statement_coercion_ddl(self):
         d1 = DDL("hi")
-        is_(expect(roles.CoerceTextStatementRole, d1), d1)
+        is_(expect(roles.StatementRole, d1), d1)
 
     def test_strict_from_clause_role(self):
         stmt = select(t).subquery()
@@ -325,7 +313,7 @@ class RoleTest(fixtures.TestBase):
 
     def test_statement_coercion_sequence(self):
         s1 = Sequence("hi")
-        is_(expect(roles.CoerceTextStatementRole, s1), s1)
+        is_(expect(roles.StatementRole, s1), s1)
 
     def test_columns_clause_role(self):
         is_(expect(roles.ColumnsClauseRole, t.c.q), t.c.q)