From ac2ed15740629967e7fe004d3a7369ccf97aac46 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Mon, 5 Apr 2021 22:14:18 -0400 Subject: Disallow AliasedReturnsRows from execution 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 --- lib/sqlalchemy/sql/base.py | 2 +- lib/sqlalchemy/sql/coercions.py | 8 ++------ lib/sqlalchemy/sql/elements.py | 4 ++-- lib/sqlalchemy/sql/lambdas.py | 10 ++-------- lib/sqlalchemy/sql/roles.py | 20 +++++++------------- lib/sqlalchemy/sql/selectable.py | 21 +++++++++++++++++---- 6 files changed, 31 insertions(+), 34 deletions(-) (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py index ac9d66970..81685dfe0 100644 --- a/lib/sqlalchemy/sql/base.py +++ b/lib/sqlalchemy/sql/base.py @@ -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 diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py index 35ac1a5ba..c00262fd5 100644 --- a/lib/sqlalchemy/sql/coercions.py +++ b/lib/sqlalchemy/sql/coercions.py @@ -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 ) diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index c48338303..dfa6c0f8f 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -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 ) diff --git a/lib/sqlalchemy/sql/lambdas.py b/lib/sqlalchemy/sql/lambdas.py index ebf576c8f..06db8f95e 100644 --- a/lib/sqlalchemy/sql/lambdas.py +++ b/lib/sqlalchemy/sql/lambdas.py @@ -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 diff --git a/lib/sqlalchemy/sql/roles.py b/lib/sqlalchemy/sql/roles.py index 1c8276eb6..7d64e8382 100644 --- a/lib/sqlalchemy/sql/roles.py +++ b/lib/sqlalchemy/sql/roles.py @@ -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): diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index a2e5780f8..f12744cfa 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -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 -- cgit v1.2.1