From 91f376692d472a5bf0c4b4033816250ec1ce3ab6 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 26 Jun 2020 16:15:19 -0400 Subject: Add future=True to create_engine/Session; unify select() Several weeks of using the future_select() construct has led to the proposal there be just one select() construct again which features the new join() method, and otherwise accepts both the 1.x and 2.x argument styles. This would make migration simpler and reduce confusion. However, confusion may be increased by the fact that select().join() is different Current thinking is we may be better off with a few hard behavioral changes to old and relatively unknown APIs rather than trying to play both sides within two extremely similar but subtly different APIs. At the moment, the .join() thing seems to be the only behavioral change that occurs without the user taking any explicit steps. Session.execute() will still behave the old way as we are adding a future flag. This change also adds the "future" flag to Session() and session.execute(), so that interpretation of the incoming statement, as well as that the new style result is returned, does not occur for existing applications unless they add the use of this flag. The change in general is moving the "removed in 2.0" system further along where we want the test suite to fully pass even if the SQLALCHEMY_WARN_20 flag is set. Get many tests to pass when SQLALCHEMY_WARN_20 is set; this should be ongoing after this patch merges. Improve the RemovedIn20 warning; these are all deprecated "since" 1.4, so ensure that's what the messages read. Make sure the inforamtion link is on all warnings. Add deprecation warnings for parameters present and add warnings to all FromClause.select() types of methods. Fixes: #5379 Fixes: #5284 Change-Id: I765a0b912b3dcd0e995426427d8bb7997cbffd51 References: #5159 --- lib/sqlalchemy/orm/context.py | 28 ++++++++++++++++-------- lib/sqlalchemy/orm/loading.py | 1 + lib/sqlalchemy/orm/persistence.py | 15 +++++++------ lib/sqlalchemy/orm/query.py | 9 +++++--- lib/sqlalchemy/orm/relationships.py | 16 +++++++++----- lib/sqlalchemy/orm/session.py | 43 +++++++++++++++++++++++++++++++++++-- 6 files changed, 86 insertions(+), 26 deletions(-) (limited to 'lib/sqlalchemy/orm') diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py index 09163d4e9..d5f001db1 100644 --- a/lib/sqlalchemy/orm/context.py +++ b/lib/sqlalchemy/orm/context.py @@ -25,6 +25,7 @@ from ..sql import expression from ..sql import roles from ..sql import util as sql_util from ..sql import visitors +from ..sql.base import _entity_namespace_key from ..sql.base import _select_iterables from ..sql.base import CacheableOptions from ..sql.base import CompileState @@ -241,8 +242,6 @@ class ORMCompileState(CompileState): # were passed to session.execute: # session.execute(legacy_select([User.id, User.name])) # see test_query->test_legacy_tuple_old_select - if not statement._is_future: - return result load_options = execution_options.get( "_sa_orm_load_options", QueryContext.default_load_options @@ -399,6 +398,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): compound_eager_adapter = None correlate = None + correlate_except = None _where_criteria = () _having_criteria = () @@ -406,9 +406,6 @@ class ORMSelectCompileState(ORMCompileState, SelectState): def create_for_statement(cls, statement, compiler, **kw): """compiler hook, we arrive here from compiler.visit_select() only.""" - if not statement._is_future: - return SelectState(statement, compiler, **kw) - if compiler is not None: toplevel = not compiler.stack compiler._rewrites_selected_columns = True @@ -592,6 +589,13 @@ class ORMSelectCompileState(ORMCompileState, SelectState): for s in query._correlate ) ) + elif query._correlate_except: + self.correlate_except = tuple( + util.flatten_iterator( + sql_util.surface_selectables(s) if s is not None else None + for s in query._correlate_except + ) + ) elif not query._auto_correlate: self.correlate = (None,) @@ -827,6 +831,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): hints=self.select_statement._hints, statement_hints=self.select_statement._statement_hints, correlate=self.correlate, + correlate_except=self.correlate_except, **self._select_args ) @@ -902,6 +907,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): hints=self.select_statement._hints, statement_hints=self.select_statement._statement_hints, correlate=self.correlate, + correlate_except=self.correlate_except, **self._select_args ) @@ -921,6 +927,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): hints, statement_hints, correlate, + correlate_except, limit_clause, offset_clause, distinct, @@ -972,6 +979,11 @@ class ORMSelectCompileState(ORMCompileState, SelectState): if correlate: statement.correlate.non_generative(statement, *correlate) + if correlate_except: + statement.correlate_except.non_generative( + statement, *correlate_except + ) + return statement def _adapt_polymorphic_element(self, element): @@ -1222,7 +1234,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): # string given, e.g. query(Foo).join("bar"). # we look to the left entity or what we last joined # towards - onclause = sql.util._entity_namespace_key( + onclause = _entity_namespace_key( inspect(self._joinpoint_zero()), onclause ) @@ -1243,9 +1255,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): info = inspect(jp0) if getattr(info, "mapper", None) is onclause._parententity: - onclause = sql.util._entity_namespace_key( - info, onclause.key - ) + onclause = _entity_namespace_key(info, onclause.key) # legacy ^^^^^^^^^^^^^^^^^^^^^^^^^^^ if isinstance(onclause, interfaces.PropComparator): diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index abb8ce32d..55c2b79f5 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -431,6 +431,7 @@ def load_on_pk_identity( params=load_options._params, execution_options={"_sa_orm_load_options": load_options}, bind_arguments=bind_arguments, + future=True, ) .unique() .scalars() diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index cbe7bde33..0cc16b96e 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -29,17 +29,17 @@ from .. import future from .. import sql from .. import util from ..engine import result as _result -from ..future import select as future_select from ..sql import coercions from ..sql import expression from ..sql import operators from ..sql import roles +from ..sql import select +from ..sql.base import _entity_namespace_key from ..sql.base import CompileState from ..sql.base import Options from ..sql.dml import DeleteDMLState from ..sql.dml import UpdateDMLState from ..sql.elements import BooleanClauseList -from ..sql.util import _entity_namespace_key def _bulk_insert( @@ -887,7 +887,7 @@ def _emit_update_statements( ) ) - stmt = table.update(clauses) + stmt = table.update().where(clauses) return stmt cached_stmt = base_mapper._memo(("update", table), update_stmt) @@ -1280,7 +1280,7 @@ def _emit_post_update_statements( ) ) - stmt = table.update(clauses) + stmt = table.update().where(clauses) if mapper.version_id_col is not None: stmt = stmt.return_defaults(mapper.version_id_col) @@ -1394,7 +1394,7 @@ def _emit_delete_statements( ) ) - return table.delete(clauses) + return table.delete().where(clauses) statement = base_mapper._memo(("delete", table), delete_stmt) for connection, recs in groupby(delete, lambda rec: rec[1]): # connection @@ -1940,7 +1940,7 @@ class BulkUDCompileState(CompileState): for k, v in iterator: if mapper: if isinstance(k, util.string_types): - desc = sql.util._entity_namespace_key(mapper, k) + desc = _entity_namespace_key(mapper, k) values.extend(desc._bulk_update_tuples(v)) elif "entity_namespace" in k._annotations: k_anno = k._annotations @@ -1989,7 +1989,7 @@ class BulkUDCompileState(CompileState): ): mapper = update_options._subject_mapper - select_stmt = future_select( + select_stmt = select( *(mapper.primary_key + (mapper.select_identity_token,)) ) select_stmt._where_criteria = statement._where_criteria @@ -2007,6 +2007,7 @@ class BulkUDCompileState(CompileState): execution_options, bind_arguments, _add_event=skip_for_full_returning, + future=True, ) matched_rows = result.fetchall() diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index 1ca65c733..acc76094b 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -45,12 +45,13 @@ from .. import inspection from .. import log from .. import sql from .. import util -from ..future.selectable import Select as FutureSelect from ..sql import coercions from ..sql import expression from ..sql import roles +from ..sql import Select from ..sql import util as sql_util from ..sql.annotation import SupportsCloneAnnotations +from ..sql.base import _entity_namespace_key from ..sql.base import _generative from ..sql.base import Executable from ..sql.selectable import _SelectFromElements @@ -61,7 +62,6 @@ from ..sql.selectable import HasSuffixes from ..sql.selectable import LABEL_STYLE_NONE from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL from ..sql.selectable import SelectStatementGrouping -from ..sql.util import _entity_namespace_key from ..sql.visitors import InternalTraversal from ..util import collections_abc @@ -419,7 +419,7 @@ class Query( stmt._propagate_attrs = self._propagate_attrs else: # Query / select() internal attributes are 99% cross-compatible - stmt = FutureSelect.__new__(FutureSelect) + stmt = Select.__new__(Select) stmt.__dict__.update(self.__dict__) stmt.__dict__.update( _label_style=self._label_style, @@ -2836,6 +2836,7 @@ class Query( statement, params, execution_options={"_sa_orm_load_options": self.load_options}, + future=True, ) # legacy: automatically set scalars, unique @@ -3209,6 +3210,7 @@ class Query( delete_, self.load_options._params, execution_options={"synchronize_session": synchronize_session}, + future=True, ) bulk_del.result = result self.session.dispatch.after_bulk_delete(bulk_del) @@ -3363,6 +3365,7 @@ class Query( upd, self.load_options._params, execution_options={"synchronize_session": synchronize_session}, + future=True, ) bulk_ud.result = result self.session.dispatch.after_bulk_update(bulk_ud) diff --git a/lib/sqlalchemy/orm/relationships.py b/lib/sqlalchemy/orm/relationships.py index bedc54153..0be15260e 100644 --- a/lib/sqlalchemy/orm/relationships.py +++ b/lib/sqlalchemy/orm/relationships.py @@ -1352,12 +1352,18 @@ class RelationshipProperty(StrategizedProperty): crit = j & sql.True_._ifnone(criterion) if secondary is not None: - ex = sql.exists( - [1], crit, from_obj=[dest, secondary] - ).correlate_except(dest, secondary) + ex = ( + sql.exists(1) + .where(crit) + .select_from(dest, secondary) + .correlate_except(dest, secondary) + ) else: - ex = sql.exists([1], crit, from_obj=dest).correlate_except( - dest + ex = ( + sql.exists(1) + .where(crit) + .select_from(dest) + .correlate_except(dest) ) return ex diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index abc990f7b..f4f7374e4 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -118,6 +118,7 @@ class ORMExecuteState(util.MemoizedSlots): "_compile_state_cls", "_starting_event_idx", "_events_todo", + "_future", ) def __init__( @@ -129,6 +130,7 @@ class ORMExecuteState(util.MemoizedSlots): bind_arguments, compile_state_cls, events_todo, + future, ): self.session = session self.statement = statement @@ -137,6 +139,7 @@ class ORMExecuteState(util.MemoizedSlots): self.bind_arguments = bind_arguments self._compile_state_cls = compile_state_cls self._events_todo = list(events_todo) + self._future = future def _remaining_events(self): return self._events_todo[self._starting_event_idx + 1 :] @@ -212,6 +215,7 @@ class ORMExecuteState(util.MemoizedSlots): _execution_options, _bind_arguments, _parent_execute_state=self, + future=self._future, ) @property @@ -924,6 +928,7 @@ class Session(_SessionClassMethods): self, bind=None, autoflush=True, + future=False, expire_on_commit=True, autocommit=False, twophase=False, @@ -1039,6 +1044,26 @@ class Session(_SessionClassMethods): so that all attribute/object access subsequent to a completed transaction will load from the most recent database state. + :param future: if True, use 2.0 style behavior for the + :meth:`_orm.Session.execute` method. This includes that the + :class:`_engine.Result` object returned will return new-style + tuple rows, as well as that Core constructs such as + :class:`_sql.Select`, + :class:`_sql.Update` and :class:`_sql.Delete` will be interpreted + in an ORM context if they are made against ORM entities rather than + plain :class:`.Table` metadata objects. + + The "future" flag is also available on a per-execution basis + using the :paramref:`_orm.Session.execute.future` flag. + + .. versionadded:: 1.4 + + .. seealso:: + + :ref:`migration_20_toplevel` + + :ref:`migration_20_result_rows` + :param info: optional dictionary of arbitrary data to be associated with this :class:`.Session`. Is available via the :attr:`.Session.info` attribute. Note the dictionary is copied at @@ -1071,6 +1096,7 @@ class Session(_SessionClassMethods): self._flushing = False self._warn_on_events = False self._transaction = None + self.future = future self.hash_key = _new_sessionid() self.autoflush = autoflush self.autocommit = autocommit @@ -1387,6 +1413,7 @@ class Session(_SessionClassMethods): params=None, execution_options=util.immutabledict(), bind_arguments=None, + future=False, _parent_execute_state=None, _add_event=None, **kw @@ -1493,6 +1520,14 @@ class Session(_SessionClassMethods): Contents of this dictionary are passed to the :meth:`.Session.get_bind` method. + :param future: + Use future style execution for this statement. This is + the same effect as the :paramref:`_orm.Session.future` flag, + except at the level of this single statement execution. See + that flag for details. + + .. versionadded:: 1.4 + :param mapper: deprecated; use the bind_arguments dictionary @@ -1518,15 +1553,18 @@ class Session(_SessionClassMethods): """ statement = coercions.expect(roles.CoerceTextStatementRole, statement) + future = future or self.future + if not bind_arguments: bind_arguments = kw elif kw: bind_arguments.update(kw) - if ( + if future and ( statement._propagate_attrs.get("compile_state_plugin", None) == "orm" ): + # note that even without "future" mode, we need compile_state_cls = CompileState._get_plugin_class_for_plugin( statement, "orm" ) @@ -1547,7 +1585,7 @@ class Session(_SessionClassMethods): ) else: bind_arguments.setdefault("clause", statement) - if statement._is_future: + if future: execution_options = util.immutabledict().merge_with( execution_options, {"future_result": True} ) @@ -1568,6 +1606,7 @@ class Session(_SessionClassMethods): bind_arguments, compile_state_cls, events_todo, + future, ) for idx, fn in enumerate(events_todo): orm_exec_state._starting_event_idx = idx -- cgit v1.2.1