diff options
author | mike bayer <mike_mp@zzzcomputing.com> | 2020-07-11 18:59:14 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@bbpush.zzzcomputing.com> | 2020-07-11 18:59:14 +0000 |
commit | 6ee643d723e8d65fb4bd3c8848b70693966ff3e5 (patch) | |
tree | da343aa55496aa48332f5d639bb2798eedfcf1f8 /lib | |
parent | 9f6493a8951e58e36b37e31a2787c426ffe04451 (diff) | |
parent | 5de0f1cf50cc0170d8ea61304e7b887259ab577b (diff) | |
download | sqlalchemy-6ee643d723e8d65fb4bd3c8848b70693966ff3e5.tar.gz |
Merge "Convert remaining ORM APIs to support 2.0 style"
Diffstat (limited to 'lib')
-rw-r--r-- | lib/sqlalchemy/engine/base.py | 34 | ||||
-rw-r--r-- | lib/sqlalchemy/engine/result.py | 3 | ||||
-rw-r--r-- | lib/sqlalchemy/ext/baked.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/__init__.py | 10 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/context.py | 59 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/dynamic.py | 304 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/loading.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/persistence.py | 17 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/query.py | 446 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/session.py | 731 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/strategies.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/orm/util.py | 105 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/base.py | 74 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/coercions.py | 12 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/elements.py | 62 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/operators.py | 32 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/selectable.py | 166 | ||||
-rw-r--r-- | lib/sqlalchemy/testing/assertions.py | 3 |
18 files changed, 1230 insertions, 836 deletions
diff --git a/lib/sqlalchemy/engine/base.py b/lib/sqlalchemy/engine/base.py index 6bc9588ad..2d672099b 100644 --- a/lib/sqlalchemy/engine/base.py +++ b/lib/sqlalchemy/engine/base.py @@ -759,6 +759,40 @@ class Connection(Connectable): return self._transaction is not None and self._transaction.is_active + def in_nested_transaction(self): + """Return True if a transaction is in progress.""" + if self.__branch_from is not None: + return self.__branch_from.in_nested_transaction() + + return ( + self._nested_transaction is not None + and self._nested_transaction.is_active + ) + + def get_transaction(self): + """Return the current root transaction in progress, if any. + + .. versionadded:: 1.4 + + """ + + if self.__branch_from is not None: + return self.__branch_from.get_transaction() + + return self._transaction + + def get_nested_transaction(self): + """Return the current nested transaction in progress, if any. + + .. versionadded:: 1.4 + + """ + if self.__branch_from is not None: + + return self.__branch_from.get_nested_transaction() + + return self._nested_transaction + def _begin_impl(self, transaction): assert not self.__branch_from diff --git a/lib/sqlalchemy/engine/result.py b/lib/sqlalchemy/engine/result.py index 7df17cf22..db546380e 100644 --- a/lib/sqlalchemy/engine/result.py +++ b/lib/sqlalchemy/engine/result.py @@ -282,7 +282,8 @@ class Result(InPlaceGenerative): updated usage model and calling facade for SQLAlchemy Core and SQLAlchemy ORM. In Core, it forms the basis of the :class:`.CursorResult` object which replaces the previous - :class:`.ResultProxy` interface. + :class:`.ResultProxy` interface. When using the ORM, a higher level + object called :class:`.ChunkedIteratorResult` is normally used. """ diff --git a/lib/sqlalchemy/ext/baked.py b/lib/sqlalchemy/ext/baked.py index fc6623609..e642a83d5 100644 --- a/lib/sqlalchemy/ext/baked.py +++ b/lib/sqlalchemy/ext/baked.py @@ -234,7 +234,7 @@ class BakedQuery(object): # used by the Connection, which in itself is more expensive to # generate than what BakedQuery was able to provide in 1.3 and prior - if statement.compile_options._bake_ok: + if statement._compile_options._bake_ok: self._bakery[self._effective_key(session)] = ( query, statement, diff --git a/lib/sqlalchemy/orm/__init__.py b/lib/sqlalchemy/orm/__init__.py index fabb095a2..32ec60322 100644 --- a/lib/sqlalchemy/orm/__init__.py +++ b/lib/sqlalchemy/orm/__init__.py @@ -90,8 +90,13 @@ def create_session(bind=None, **kwargs): create_session(). """ + + if kwargs.get("future", False): + kwargs.setdefault("autocommit", False) + else: + kwargs.setdefault("autocommit", True) + kwargs.setdefault("autoflush", False) - kwargs.setdefault("autocommit", True) kwargs.setdefault("expire_on_commit", False) return Session(bind=bind, **kwargs) @@ -267,12 +272,15 @@ contains_alias = public_factory(AliasOption, ".orm.contains_alias") def __go(lcls): global __all__ + global AppenderQuery from .. import util as sa_util # noqa from . import dynamic # noqa from . import events # noqa from . import loading # noqa import inspect as _inspect + from .dynamic import AppenderQuery + __all__ = sorted( name for name, obj in lcls.items() diff --git a/lib/sqlalchemy/orm/context.py b/lib/sqlalchemy/orm/context.py index d5f001db1..55a6b4cd2 100644 --- a/lib/sqlalchemy/orm/context.py +++ b/lib/sqlalchemy/orm/context.py @@ -188,7 +188,7 @@ class ORMCompileState(CompileState): raise NotImplementedError() @classmethod - def get_column_descriptions(self, statement): + def get_column_descriptions(cls, statement): return _column_descriptions(statement) @classmethod @@ -204,8 +204,14 @@ class ORMCompileState(CompileState): if is_reentrant_invoke: return statement, execution_options - load_options = execution_options.get( - "_sa_orm_load_options", QueryContext.default_load_options + ( + load_options, + execution_options, + ) = QueryContext.default_load_options.from_execution_options( + "_sa_orm_load_options", + {"populate_existing", "autoflush", "yield_per"}, + execution_options, + statement._execution_options, ) bind_arguments["clause"] = statement @@ -246,6 +252,7 @@ class ORMCompileState(CompileState): load_options = execution_options.get( "_sa_orm_load_options", QueryContext.default_load_options ) + querycontext = QueryContext( compile_state, statement, @@ -304,7 +311,7 @@ class ORMFromStatementCompileState(ORMCompileState): self._primary_entity = None self.use_legacy_query_style = ( - statement_container.compile_options._use_legacy_query_style + statement_container._compile_options._use_legacy_query_style ) self.statement_container = self.select_statement = statement_container self.requested_statement = statement = statement_container.element @@ -315,9 +322,9 @@ class ORMFromStatementCompileState(ORMCompileState): _QueryEntity.to_compile_state(self, statement_container._raw_columns) - self.compile_options = statement_container.compile_options + self.compile_options = statement_container._compile_options - self.current_path = statement_container.compile_options._current_path + self.current_path = statement_container._compile_options._current_path if toplevel and statement_container._with_options: self.attributes = {"_unbound_load_dedupes": set()} @@ -416,8 +423,8 @@ class ORMSelectCompileState(ORMCompileState, SelectState): # if we are a select() that was never a legacy Query, we won't # have ORM level compile options. - statement.compile_options = cls.default_compile_options.safe_merge( - statement.compile_options + statement._compile_options = cls.default_compile_options.safe_merge( + statement._compile_options ) self = cls.__new__(cls) @@ -434,20 +441,20 @@ class ORMSelectCompileState(ORMCompileState, SelectState): # indicates this select() came from Query.statement self.for_statement = ( for_statement - ) = select_statement.compile_options._for_statement + ) = select_statement._compile_options._for_statement if not for_statement and not toplevel: # for subqueries, turn off eagerloads. # if "for_statement" mode is set, Query.subquery() # would have set this flag to False already if that's what's # desired - select_statement.compile_options += { + select_statement._compile_options += { "_enable_eagerloads": False, } # generally if we are from Query or directly from a select() self.use_legacy_query_style = ( - select_statement.compile_options._use_legacy_query_style + select_statement._compile_options._use_legacy_query_style ) self._entities = [] @@ -457,15 +464,15 @@ class ORMSelectCompileState(ORMCompileState, SelectState): self._no_yield_pers = set() # legacy: only for query.with_polymorphic() - if select_statement.compile_options._with_polymorphic_adapt_map: + if select_statement._compile_options._with_polymorphic_adapt_map: self._with_polymorphic_adapt_map = dict( - select_statement.compile_options._with_polymorphic_adapt_map + select_statement._compile_options._with_polymorphic_adapt_map ) self._setup_with_polymorphics() _QueryEntity.to_compile_state(self, select_statement._raw_columns) - self.compile_options = select_statement.compile_options + self.compile_options = select_statement._compile_options # determine label style. we can make different decisions here. # at the moment, trying to see if we can always use DISAMBIGUATE_ONLY @@ -479,7 +486,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): else: self.label_style = self.select_statement._label_style - self.current_path = select_statement.compile_options._current_path + self.current_path = select_statement._compile_options._current_path self.eager_order_by = () @@ -668,7 +675,7 @@ class ORMSelectCompileState(ORMCompileState, SelectState): self._polymorphic_adapters = {} compile_options = cls.default_compile_options.safe_merge( - query.compile_options + query._compile_options ) # legacy: only for query.with_polymorphic() if compile_options._with_polymorphic_adapt_map: @@ -711,6 +718,26 @@ class ORMSelectCompileState(ORMCompileState, SelectState): for elem in _select_iterables([element]): yield elem + @classmethod + @util.preload_module("sqlalchemy.orm.query") + def from_statement(cls, statement, from_statement): + query = util.preloaded.orm_query + + from_statement = coercions.expect( + roles.SelectStatementRole, + from_statement, + apply_propagate_attrs=statement, + ) + + stmt = query.FromStatement(statement._raw_columns, from_statement) + stmt.__dict__.update( + _with_options=statement._with_options, + _with_context_options=statement._with_context_options, + _execution_options=statement._execution_options, + _propagate_attrs=statement._propagate_attrs, + ) + return stmt + def _setup_with_polymorphics(self): # legacy: only for query.with_polymorphic() for ext_info, wp in self._with_polymorphic_adapt_map.items(): diff --git a/lib/sqlalchemy/orm/dynamic.py b/lib/sqlalchemy/orm/dynamic.py index d15127563..7832152a2 100644 --- a/lib/sqlalchemy/orm/dynamic.py +++ b/lib/sqlalchemy/orm/dynamic.py @@ -23,7 +23,12 @@ from . import util as orm_util from .query import Query from .. import exc from .. import log +from .. import sql from .. import util +from ..engine import result as _result +from ..sql import selectable +from ..sql.base import _generative +from ..sql.base import Generative @log.class_logger @@ -74,7 +79,6 @@ class DynamicAttributeImpl(attributes.AttributeImpl): dispatch, target_mapper, order_by, - query_class=None, **kw ): super(DynamicAttributeImpl, self).__init__( @@ -82,12 +86,7 @@ class DynamicAttributeImpl(attributes.AttributeImpl): ) self.target_mapper = target_mapper self.order_by = order_by - if not query_class: - self.query_class = AppenderQuery - elif AppenderMixin in query_class.mro(): - self.query_class = query_class - else: - self.query_class = mixin_user_query(query_class) + self.query_class = AppenderQuery def get(self, state, dict_, passive=attributes.PASSIVE_OFF): if not passive & attributes.SQL_OK: @@ -259,15 +258,26 @@ class DynamicAttributeImpl(attributes.AttributeImpl): self.remove(state, dict_, value, initiator, passive=passive) -class AppenderMixin(object): - query_class = None +class AppenderQuery(Generative): + """A dynamic query that supports basic collection storage operations.""" def __init__(self, attr, state): - super(AppenderMixin, self).__init__(attr.target_mapper, None) + + # this can be select() except for aliased=True flag on join() + # and corresponding behaviors on select(). + self._is_core = False + self._statement = Query([attr.target_mapper], None) + + # self._is_core = True + # self._statement = sql.select(attr.target_mapper)._set_label_style( + # selectable.LABEL_STYLE_TABLENAME_PLUS_COL + # ) + + self._autoflush = True self.instance = instance = state.obj() self.attr = attr - mapper = object_mapper(instance) + self.mapper = mapper = object_mapper(instance) prop = mapper._props[self.attr.key] if prop.secondary is not None: @@ -277,29 +287,154 @@ class AppenderMixin(object): # is in the FROM. So we purposely put the mapper selectable # in _from_obj[0] to ensure a user-defined join() later on # doesn't fail, and secondary is then in _from_obj[1]. - self._from_obj = (prop.mapper.selectable, prop.secondary) + self._statement = self._statement.select_from( + prop.mapper.selectable, prop.secondary + ) - self._where_criteria += ( + self._statement = self._statement.where( prop._with_parent(instance, alias_secondary=False), ) if self.attr.order_by: - if ( - self._order_by_clauses is False - or self._order_by_clauses is None - ): - self._order_by_clauses = tuple(self.attr.order_by) - else: - self._order_by_clauses = self._order_by_clauses + tuple( - self.attr.order_by - ) + self._statement = self._statement.order_by(*self.attr.order_by) + + @_generative + def autoflush(self, setting): + """Set autoflush to a specific setting. + + Note that a Session with autoflush=False will + not autoflush, even if this flag is set to True at the + Query level. Therefore this flag is usually used only + to disable autoflush for a specific Query. + + """ + self._autoflush = setting + + @property + def statement(self): + """Return the Core statement represented by this + :class:`.AppenderQuery`. + + """ + if self._is_core: + return self._statement._set_label_style( + selectable.LABEL_STYLE_DISAMBIGUATE_ONLY + ) + + else: + return self._statement.statement + + def filter(self, *criteria): + """A synonym for the :meth:`_orm.AppenderQuery.where` method.""" + + return self.where(*criteria) + + @_generative + def where(self, *criteria): + r"""Apply the given WHERE criterion, using SQL expressions. + + Equivalent to :meth:`.Select.where`. + + """ + self._statement = self._statement.where(*criteria) + + @_generative + def order_by(self, *criteria): + r"""Apply the given ORDER BY criterion, using SQL expressions. + + Equivalent to :meth:`.Select.order_by`. + + """ + self._statement = self._statement.order_by(*criteria) + + @_generative + def filter_by(self, **kwargs): + r"""Apply the given filtering criterion using keyword expressions. + + Equivalent to :meth:`.Select.filter_by`. + + """ + self._statement = self._statement.filter_by(**kwargs) + + @_generative + def join(self, target, *props, **kwargs): + r"""Create a SQL JOIN against this + object's criterion. + + Equivalent to :meth:`.Select.join`. + """ + + self._statement = self._statement.join(target, *props, **kwargs) + + @_generative + def outerjoin(self, target, *props, **kwargs): + r"""Create a SQL LEFT OUTER JOIN against this + object's criterion. + + Equivalent to :meth:`.Select.outerjoin`. + + """ + + self._statement = self._statement.outerjoin(target, *props, **kwargs) + + def scalar(self): + """Return the first element of the first result or None + if no rows present. If multiple rows are returned, + raises MultipleResultsFound. + + Equivalent to :meth:`_query.Query.scalar`. + + .. versionadded:: 1.1.6 + + """ + return self._iter().scalar() + + def first(self): + """Return the first row. + + Equivalent to :meth:`_query.Query.first`. + + """ + + # replicates limit(1) behavior + if self._statement is not None: + return self._iter().first() + else: + return self.limit(1)._iter().first() + + def one(self): + """Return exactly one result or raise an exception. + + Equivalent to :meth:`_query.Query.one`. + + """ + return self._iter().one() + + def one_or_none(self): + """Return one or zero results, or raise an exception for multiple + rows. + + Equivalent to :meth:`_query.Query.one_or_none`. + + .. versionadded:: 1.0.9 + + """ + return self._iter().one_or_none() + + def all(self): + """Return all rows. + + Equivalent to :meth:`_query.Query.all`. + + """ + return self._iter().all() def session(self): sess = object_session(self.instance) if ( sess is not None - and self.autoflush + and self._autoflush and sess.autoflush and self.instance in sess ): @@ -311,17 +446,60 @@ class AppenderMixin(object): session = property(session, lambda s, x: None) - def __iter__(self): + def _execute(self, sess=None): + # note we're returning an entirely new Query class instance + # here without any assignment capabilities; the class of this + # query is determined by the session. + instance = self.instance + if sess is None: + sess = object_session(instance) + if sess is None: + raise orm_exc.DetachedInstanceError( + "Parent instance %s is not bound to a Session, and no " + "contextual session is established; lazy load operation " + "of attribute '%s' cannot proceed" + % (orm_util.instance_str(instance), self.attr.key) + ) + + result = sess.execute(self._statement, future=True) + result = result.scalars() + + if result._attributes.get("filtered", False): + result = result.unique() + + return result + + def _iter(self): sess = self.session if sess is None: - return iter( - self.attr._get_collection_history( - attributes.instance_state(self.instance), - attributes.PASSIVE_NO_INITIALIZE, - ).added_items - ) + instance = self.instance + state = attributes.instance_state(instance) + + if state.detached: + raise orm_exc.DetachedInstanceError( + "Parent instance %s is not bound to a Session, and no " + "contextual session is established; lazy load operation " + "of attribute '%s' cannot proceed" + % (orm_util.instance_str(instance), self.attr.key) + ) + else: + iterator = ( + (item,) + for item in self.attr._get_collection_history( + state, attributes.PASSIVE_NO_INITIALIZE, + ).added_items + ) + + row_metadata = _result.SimpleResultMetaData( + (self.mapper.class_.__name__,), [], _unique_filters=[id], + ) + + return _result.IteratorResult(row_metadata, iterator).scalars() else: - return iter(self._generate(sess)) + return self._execute(sess) + + def __iter__(self): + return iter(self._iter()) def __getitem__(self, index): sess = self.session @@ -331,9 +509,32 @@ class AppenderMixin(object): attributes.PASSIVE_NO_INITIALIZE, ).indexed(index) else: - return self._generate(sess).__getitem__(index) + return orm_util._getitem(self, index) + + def slice(self, start, stop): + """Computes the "slice" represented by + the given indices and apply as LIMIT/OFFSET. + + + """ + limit_clause, offset_clause = orm_util._make_slice( + self._statement._limit_clause, + self._statement._offset_clause, + start, + stop, + ) + self._statement = self._statement.limit(limit_clause).offset( + offset_clause + ) def count(self): + """return the 'count'. + + Equivalent to :meth:`_query.Query.count`. + + + """ + sess = self.session if sess is None: return len( @@ -343,33 +544,10 @@ class AppenderMixin(object): ).added_items ) else: - return self._generate(sess).count() - - def _generate(self, sess=None): - # note we're returning an entirely new Query class instance - # here without any assignment capabilities; the class of this - # query is determined by the session. - instance = self.instance - if sess is None: - sess = object_session(instance) - if sess is None: - raise orm_exc.DetachedInstanceError( - "Parent instance %s is not bound to a Session, and no " - "contextual session is established; lazy load operation " - "of attribute '%s' cannot proceed" - % (orm_util.instance_str(instance), self.attr.key) - ) - - if self.query_class: - query = self.query_class(self.attr.target_mapper, session=sess) - else: - query = sess.query(self.attr.target_mapper) - - query._where_criteria = self._where_criteria - query._from_obj = self._from_obj - query._order_by_clauses = self._order_by_clauses + col = sql.func.count(sql.literal_column("*")) - return query + stmt = sql.select(col).select_from(self._statement.subquery()) + return self.session.execute(stmt).scalar() def extend(self, iterator): for item in iterator: @@ -397,16 +575,6 @@ class AppenderMixin(object): ) -class AppenderQuery(AppenderMixin, Query): - """A dynamic query that supports basic collection storage operations.""" - - -def mixin_user_query(cls): - """Return a new class with AppenderQuery functionality layered over.""" - name = "Appender" + cls.__name__ - return type(name, (AppenderMixin, cls), {"query_class": cls}) - - class CollectionHistory(object): """Overrides AttributeHistory to receive append/remove events directly.""" diff --git a/lib/sqlalchemy/orm/loading.py b/lib/sqlalchemy/orm/loading.py index 55c2b79f5..8d1ae2e69 100644 --- a/lib/sqlalchemy/orm/loading.py +++ b/lib/sqlalchemy/orm/loading.py @@ -346,7 +346,7 @@ def load_on_pk_identity( load_options = QueryContext.default_load_options compile_options = ORMCompileState.default_compile_options.safe_merge( - q.compile_options + q._compile_options ) if primary_key_identity is not None: @@ -411,7 +411,7 @@ def load_on_pk_identity( # TODO: most of the compile_options that are not legacy only involve this # function, so try to see if handling of them can mostly be local to here - q.compile_options, load_options = _set_get_options( + q._compile_options, load_options = _set_get_options( compile_options, load_options, populate_existing=bool(refresh_state), diff --git a/lib/sqlalchemy/orm/persistence.py b/lib/sqlalchemy/orm/persistence.py index 1b2779c00..a78af92b9 100644 --- a/lib/sqlalchemy/orm/persistence.py +++ b/lib/sqlalchemy/orm/persistence.py @@ -1762,24 +1762,23 @@ class BulkUDCompileState(CompileState): if is_reentrant_invoke: return statement, execution_options - sync = execution_options.get("synchronize_session", None) - if sync is None: - sync = statement._execution_options.get( - "synchronize_session", None - ) - - update_options = execution_options.get( + ( + update_options, + execution_options, + ) = BulkUDCompileState.default_update_options.from_execution_options( "_sa_orm_update_options", - BulkUDCompileState.default_update_options, + {"synchronize_session"}, + execution_options, + statement._execution_options, ) + sync = update_options._synchronize_session if sync is not None: if sync not in ("evaluate", "fetch", False): raise sa_exc.ArgumentError( "Valid strategies for session synchronization " "are 'evaluate', 'fetch', False" ) - update_options += {"_synchronize_session": sync} bind_arguments["clause"] = statement try: diff --git a/lib/sqlalchemy/orm/query.py b/lib/sqlalchemy/orm/query.py index acc76094b..7bf69f99f 100644 --- a/lib/sqlalchemy/orm/query.py +++ b/lib/sqlalchemy/orm/query.py @@ -22,10 +22,10 @@ import itertools import operator import types -from . import attributes from . import exc as orm_exc from . import interfaces from . import loading +from . import util as orm_util from .base import _assertions from .context import _column_descriptions from .context import _legacy_determine_last_joined_entity @@ -121,7 +121,7 @@ class Query( _legacy_setup_joins = () _label_style = LABEL_STYLE_NONE - compile_options = ORMCompileState.default_compile_options + _compile_options = ORMCompileState.default_compile_options load_options = QueryContext.default_load_options @@ -215,7 +215,7 @@ class Query( for elem in obj ] - self.compile_options += {"_set_base_alias": set_base_alias} + self._compile_options += {"_set_base_alias": set_base_alias} self._from_obj = tuple(fa) @_generative @@ -254,7 +254,7 @@ class Query( self._from_obj = self._legacy_setup_joins = () if self._statement is not None: - self.compile_options += {"_statement": None} + self._compile_options += {"_statement": None} self._where_criteria = () self._distinct = False @@ -320,7 +320,7 @@ class Query( if load_options: self.load_options += load_options if compile_options: - self.compile_options += compile_options + self._compile_options += compile_options return self @@ -357,8 +357,8 @@ class Query( # passed into the execute process and wont generate its own cache # key; this will all occur in terms of the ORM-enabled Select. if ( - not self.compile_options._set_base_alias - and not self.compile_options._with_polymorphic_adapt_map + not self._compile_options._set_base_alias + and not self._compile_options._with_polymorphic_adapt_map ): # if we don't have legacy top level aliasing features in use # then convert to a future select() directly @@ -400,9 +400,9 @@ class Query( if new_query is not None and new_query is not self: self = new_query if not fn._bake_ok: - self.compile_options += {"_bake_ok": False} + self._compile_options += {"_bake_ok": False} - compile_options = self.compile_options + compile_options = self._compile_options compile_options += { "_for_statement": for_statement, "_use_legacy_query_style": use_legacy_query_style, @@ -413,21 +413,21 @@ class Query( stmt.__dict__.update( _with_options=self._with_options, _with_context_options=self._with_context_options, - compile_options=compile_options, + _compile_options=compile_options, _execution_options=self._execution_options, + _propagate_attrs=self._propagate_attrs, ) - stmt._propagate_attrs = self._propagate_attrs else: # Query / select() internal attributes are 99% cross-compatible stmt = Select.__new__(Select) stmt.__dict__.update(self.__dict__) stmt.__dict__.update( _label_style=self._label_style, - compile_options=compile_options, + _compile_options=compile_options, + _propagate_attrs=self._propagate_attrs, ) stmt.__dict__.pop("session", None) - stmt._propagate_attrs = self._propagate_attrs return stmt def subquery( @@ -629,7 +629,7 @@ class Query( selectable, or when using :meth:`_query.Query.yield_per`. """ - self.compile_options += {"_enable_eagerloads": value} + self._compile_options += {"_enable_eagerloads": value} @_generative def with_labels(self): @@ -710,7 +710,7 @@ class Query( query intended for the deferred load. """ - self.compile_options += {"_current_path": path} + self._compile_options += {"_current_path": path} # TODO: removed in 2.0 @_generative @@ -744,7 +744,7 @@ class Query( polymorphic_on=polymorphic_on, ) - self.compile_options = self.compile_options.add_to_element( + self._compile_options = self._compile_options.add_to_element( "_with_polymorphic_adapt_map", ((entity, inspect(wp)),) ) @@ -818,6 +818,10 @@ class Query( {"stream_results": True, "max_row_buffer": count} ) + @util.deprecated_20( + ":meth:`_orm.Query.get`", + alternative="The method is now available as :meth:`_orm.Session.get`", + ) def get(self, ident): """Return an instance based on the given primary key identifier, or ``None`` if not found. @@ -858,14 +862,6 @@ class Query( however, and will be used if the object is not yet locally present. - A lazy-loading, many-to-one attribute configured - by :func:`_orm.relationship`, using a simple - foreign-key-to-primary-key criterion, will also use an - operation equivalent to :meth:`_query.Query.get` in order to retrieve - the target value from the local identity map - before querying the database. See :doc:`/orm/loading_relationships` - for further details on relationship loading. - :param ident: A scalar, tuple, or dictionary representing the primary key. For a composite (e.g. multiple column) primary key, a tuple or dictionary should be passed. @@ -905,80 +901,22 @@ class Query( """ self._no_criterion_assertion("get", order_by=False, distinct=False) + + # we still implement _get_impl() so that baked query can override + # it return self._get_impl(ident, loading.load_on_pk_identity) def _get_impl(self, primary_key_identity, db_load_fn, identity_token=None): - - # convert composite types to individual args - if hasattr(primary_key_identity, "__composite_values__"): - primary_key_identity = primary_key_identity.__composite_values__() - mapper = self._only_full_mapper_zero("get") - - is_dict = isinstance(primary_key_identity, dict) - if not is_dict: - primary_key_identity = util.to_list( - primary_key_identity, default=(None,) - ) - - if len(primary_key_identity) != len(mapper.primary_key): - raise sa_exc.InvalidRequestError( - "Incorrect number of values in identifier to formulate " - "primary key for query.get(); primary key columns are %s" - % ",".join("'%s'" % c for c in mapper.primary_key) - ) - - if is_dict: - try: - primary_key_identity = list( - primary_key_identity[prop.key] - for prop in mapper._identity_key_props - ) - - except KeyError as err: - util.raise_( - sa_exc.InvalidRequestError( - "Incorrect names of values in identifier to formulate " - "primary key for query.get(); primary key attribute " - "names are %s" - % ",".join( - "'%s'" % prop.key - for prop in mapper._identity_key_props - ) - ), - replace_context=err, - ) - - if ( - not self.load_options._populate_existing - and not mapper.always_refresh - and self._for_update_arg is None - ): - - instance = self.session._identity_lookup( - mapper, primary_key_identity, identity_token=identity_token - ) - - if instance is not None: - self._get_existing_condition() - # reject calls for id in identity map but class - # mismatch. - if not issubclass(instance.__class__, mapper.class_): - return None - return instance - elif instance is attributes.PASSIVE_CLASS_MISMATCH: - return None - - # apply_labels() not strictly necessary, however this will ensure that - # tablename_colname style is used which at the moment is asserted - # in a lot of unit tests :) - - statement = self._statement_20().apply_labels() - return db_load_fn( - self.session, - statement, + return self.session._get_impl( + mapper, primary_key_identity, - load_options=self.load_options, + db_load_fn, + populate_existing=self.load_options._populate_existing, + with_for_update=self._for_update_arg, + options=self._with_options, + identity_token=identity_token, + execution_options=self._execution_options, ) @property @@ -1000,7 +938,7 @@ class Query( @property def _current_path(self): - return self.compile_options._current_path + return self._compile_options._current_path @_generative def correlate(self, *fromclauses): @@ -1375,7 +1313,7 @@ class Query( @_generative def _set_enable_single_crit(self, val): - self.compile_options += {"_enable_single_crit": val} + self._compile_options += {"_enable_single_crit": val} @_generative def _from_selectable(self, fromclause, set_entity_from=True): @@ -1394,7 +1332,7 @@ class Query( ): self.__dict__.pop(attr, None) self._set_select_from([fromclause], set_entity_from) - self.compile_options += { + self._compile_options += { "_enable_single_crit": False, "_statement": None, } @@ -1404,7 +1342,7 @@ class Query( # legacy. see test/orm/test_froms.py for various # "oldstyle" tests that rely on this and the correspoinding # "newtyle" that do not. - self.compile_options += {"_orm_only_from_obj_alias": False} + self._compile_options += {"_orm_only_from_obj_alias": False} @util.deprecated( "1.4", @@ -1517,7 +1455,7 @@ class Query( """ opts = tuple(util.flatten_iterator(args)) - if self.compile_options._current_path: + if self._compile_options._current_path: for opt in opts: if opt._is_legacy_option: opt.process_query_conditionally(self) @@ -1641,6 +1579,14 @@ class Query( params = self.load_options._params.union(kwargs) self.load_options += {"_params": params} + def where(self, *criterion): + """A synonym for :meth:`.Query.filter`. + + .. versionadded:: 1.4 + + """ + return self.filter(*criterion) + @_generative @_assertions(_no_statement_condition, _no_limit_offset) def filter(self, *criterion): @@ -2204,6 +2150,7 @@ class Query( SQLAlchemy versions was the primary ORM-level joining interface. """ + aliased, from_joinpoint, isouter, full = ( kwargs.pop("aliased", False), kwargs.pop("from_joinpoint", False), @@ -2496,36 +2443,10 @@ class Query( """ self._set_select_from([from_obj], True) - self.compile_options += {"_enable_single_crit": False} + self._compile_options += {"_enable_single_crit": False} def __getitem__(self, item): - if isinstance(item, slice): - start, stop, step = util.decode_slice(item) - - if ( - isinstance(stop, int) - and isinstance(start, int) - and stop - start <= 0 - ): - return [] - - # perhaps we should execute a count() here so that we - # can still use LIMIT/OFFSET ? - elif (isinstance(start, int) and start < 0) or ( - isinstance(stop, int) and stop < 0 - ): - return list(self)[item] - - res = self.slice(start, stop) - if step is not None: - return list(res)[None : None : item.step] - else: - return list(res) - else: - if item == -1: - return list(self)[-1] - else: - return list(self[item : item + 1])[0] + return orm_util._getitem(self, item) @_generative @_assertions(_no_statement_condition) @@ -2559,46 +2480,10 @@ class Query( :meth:`_query.Query.offset` """ - # for calculated limit/offset, try to do the addition of - # values to offset in Python, howver if a SQL clause is present - # then the addition has to be on the SQL side. - if start is not None and stop is not None: - offset_clause = self._offset_or_limit_clause_asint_if_possible( - self._offset_clause - ) - if offset_clause is None: - offset_clause = 0 - if start != 0: - offset_clause = offset_clause + start - - if offset_clause == 0: - self._offset_clause = None - else: - self._offset_clause = self._offset_or_limit_clause( - offset_clause - ) - - self._limit_clause = self._offset_or_limit_clause(stop - start) - - elif start is None and stop is not None: - self._limit_clause = self._offset_or_limit_clause(stop) - elif start is not None and stop is None: - offset_clause = self._offset_or_limit_clause_asint_if_possible( - self._offset_clause - ) - if offset_clause is None: - offset_clause = 0 - - if start != 0: - offset_clause = offset_clause + start - - if offset_clause == 0: - self._offset_clause = None - else: - self._offset_clause = self._offset_or_limit_clause( - offset_clause - ) + self._limit_clause, self._offset_clause = orm_util._make_slice( + self._limit_clause, self._offset_clause, start, stop + ) @_generative @_assertions(_no_statement_condition) @@ -2607,7 +2492,7 @@ class Query( ``Query``. """ - self._limit_clause = self._offset_or_limit_clause(limit) + self._limit_clause = orm_util._offset_or_limit_clause(limit) @_generative @_assertions(_no_statement_condition) @@ -2616,31 +2501,7 @@ class Query( ``Query``. """ - self._offset_clause = self._offset_or_limit_clause(offset) - - def _offset_or_limit_clause(self, element, name=None, type_=None): - """Convert the given value to an "offset or limit" clause. - - This handles incoming integers and converts to an expression; if - an expression is already given, it is passed through. - - """ - return coercions.expect( - roles.LimitOffsetRole, element, name=name, type_=type_ - ) - - def _offset_or_limit_clause_asint_if_possible(self, clause): - """Return the offset or limit clause as a simple integer if possible, - else return the clause. - - """ - if clause is None: - return None - if hasattr(clause, "_limit_offset_value"): - value = clause._limit_offset_value - return util.asint(value) - else: - return clause + self._offset_clause = orm_util._offset_or_limit_clause(offset) @_generative @_assertions(_no_statement_condition) @@ -2723,7 +2584,7 @@ class Query( roles.SelectStatementRole, statement, apply_propagate_attrs=self ) self._statement = statement - self.compile_options += {"_statement": statement} + self._compile_options += {"_statement": statement} def first(self): """Return the first result of this ``Query`` or @@ -3088,110 +2949,22 @@ class Query( sess.query(User).filter(User.age == 25).\ delete(synchronize_session='evaluate') - .. warning:: The :meth:`_query.Query.delete` - method is a "bulk" operation, - which bypasses ORM unit-of-work automation in favor of greater - performance. **Please read all caveats and warnings below.** - - :param synchronize_session: chooses the strategy for the removal of - matched objects from the session. Valid values are: - - ``False`` - don't synchronize the session. This option is the most - efficient and is reliable once the session is expired, which - typically occurs after a commit(), or explicitly using - expire_all(). Before the expiration, objects may still remain in - the session which were in fact deleted which can lead to confusing - results if they are accessed via get() or already loaded - collections. - - ``'fetch'`` - performs a select query before the delete to find - objects that are matched by the delete query and need to be - removed from the session. Matched objects are removed from the - session. + .. warning:: - ``'evaluate'`` - Evaluate the query's criteria in Python straight - on the objects in the session. If evaluation of the criteria isn't - implemented, an error is raised. + See the section :ref:`bulk_update_delete` for important caveats + and warnings, including limitations when using bulk UPDATE + and DELETE with mapper inheritance configurations. - The expression evaluator currently doesn't account for differing - string collations between the database and Python. + :param synchronize_session: chooses the strategy to update the + attributes on objects in the session. See the section + :ref:`bulk_update_delete` for a discussion of these strategies. :return: the count of rows matched as returned by the database's "row count" feature. - .. warning:: **Additional Caveats for bulk query deletes** - - * This method does **not work for joined - inheritance mappings**, since the **multiple table - deletes are not supported by SQL** as well as that the - **join condition of an inheritance mapper is not - automatically rendered**. Care must be taken in any - multiple-table delete to first accommodate via some other means - how the related table will be deleted, as well as to - explicitly include the joining - condition between those tables, even in mappings where - this is normally automatic. E.g. if a class ``Engineer`` - subclasses ``Employee``, a DELETE against the ``Employee`` - table would look like:: - - session.query(Engineer).\ - filter(Engineer.id == Employee.id).\ - filter(Employee.name == 'dilbert').\ - delete() - - However the above SQL will not delete from the Engineer table, - unless an ON DELETE CASCADE rule is established in the database - to handle it. - - Short story, **do not use this method for joined inheritance - mappings unless you have taken the additional steps to make - this feasible**. - - * The polymorphic identity WHERE criteria is **not** included - for single- or - joined- table updates - this must be added **manually** even - for single table inheritance. - - * The method does **not** offer in-Python cascading of - relationships - it is assumed that ON DELETE CASCADE/SET - NULL/etc. is configured for any foreign key references - which require it, otherwise the database may emit an - integrity violation if foreign key references are being - enforced. - - After the DELETE, dependent objects in the - :class:`.Session` which were impacted by an ON DELETE - may not contain the current state, or may have been - deleted. This issue is resolved once the - :class:`.Session` is expired, which normally occurs upon - :meth:`.Session.commit` or can be forced by using - :meth:`.Session.expire_all`. Accessing an expired - object whose row has been deleted will invoke a SELECT - to locate the row; when the row is not found, an - :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is - raised. - - * The ``'fetch'`` strategy results in an additional - SELECT statement emitted and will significantly reduce - performance. - - * The ``'evaluate'`` strategy performs a scan of - all matching objects within the :class:`.Session`; if the - contents of the :class:`.Session` are expired, such as - via a proceeding :meth:`.Session.commit` call, **this will - result in SELECT queries emitted for every matching object**. - - * The :meth:`.MapperEvents.before_delete` and - :meth:`.MapperEvents.after_delete` - events **are not invoked** from this method. Instead, the - :meth:`.SessionEvents.after_bulk_delete` method is provided to - act upon a mass DELETE of entity rows. - .. seealso:: - :meth:`_query.Query.update` - - :ref:`inserts_and_updates` - Core SQL tutorial + :ref:`bulk_update_delete` """ @@ -3231,12 +3004,11 @@ class Query( sess.query(User).filter(User.age == 25).\ update({"age": User.age - 10}, synchronize_session='evaluate') + .. warning:: - .. warning:: The :meth:`_query.Query.update` - method is a "bulk" operation, - which bypasses ORM unit-of-work automation in favor of greater - performance. **Please read all caveats and warnings below.** - + See the section :ref:`bulk_update_delete` for important caveats + and warnings, including limitations when using bulk UPDATE + and DELETE with mapper inheritance configurations. :param values: a dictionary with attributes names, or alternatively mapped attributes or SQL expressions, as keys, and literal @@ -3248,31 +3020,9 @@ class Query( flag is passed to the :paramref:`.Query.update.update_args` dictionary as well. - .. versionchanged:: 1.0.0 - string names in the values dictionary - are now resolved against the mapped entity; previously, these - strings were passed as literal column names with no mapper-level - translation. - :param synchronize_session: chooses the strategy to update the - attributes on objects in the session. Valid values are: - - ``False`` - don't synchronize the session. This option is the most - efficient and is reliable once the session is expired, which - typically occurs after a commit(), or explicitly using - expire_all(). Before the expiration, updated objects may still - remain in the session with stale values on their attributes, which - can lead to confusing results. - - ``'fetch'`` - performs a select query before the update to find - objects that are matched by the update query. The updated - attributes are expired on matched objects. - - ``'evaluate'`` - Evaluate the Query's criteria in Python straight - on the objects in the session. If evaluation of the criteria isn't - implemented, an exception is raised. - - The expression evaluator currently doesn't account for differing - string collations between the database and Python. + attributes on objects in the session. See the section + :ref:`bulk_update_delete` for a discussion of these strategies. :param update_args: Optional dictionary, if present will be passed to the underlying :func:`_expression.update` @@ -3281,70 +3031,14 @@ class Query( as ``mysql_limit``, as well as other special arguments such as :paramref:`~sqlalchemy.sql.expression.update.preserve_parameter_order`. - .. versionadded:: 1.0.0 - :return: the count of rows matched as returned by the database's "row count" feature. - .. warning:: **Additional Caveats for bulk query updates** - - * The method does **not** offer in-Python cascading of - relationships - it is assumed that ON UPDATE CASCADE is - configured for any foreign key references which require - it, otherwise the database may emit an integrity - violation if foreign key references are being enforced. - - After the UPDATE, dependent objects in the - :class:`.Session` which were impacted by an ON UPDATE - CASCADE may not contain the current state; this issue is - resolved once the :class:`.Session` is expired, which - normally occurs upon :meth:`.Session.commit` or can be - forced by using :meth:`.Session.expire_all`. - - * The ``'fetch'`` strategy results in an additional - SELECT statement emitted and will significantly reduce - performance. - - * The ``'evaluate'`` strategy performs a scan of - all matching objects within the :class:`.Session`; if the - contents of the :class:`.Session` are expired, such as - via a proceeding :meth:`.Session.commit` call, **this will - result in SELECT queries emitted for every matching object**. - - * The method supports multiple table updates, as detailed - in :ref:`multi_table_updates`, and this behavior does - extend to support updates of joined-inheritance and - other multiple table mappings. However, the **join - condition of an inheritance mapper is not - automatically rendered**. Care must be taken in any - multiple-table update to explicitly include the joining - condition between those tables, even in mappings where - this is normally automatic. E.g. if a class ``Engineer`` - subclasses ``Employee``, an UPDATE of the ``Engineer`` - local table using criteria against the ``Employee`` - local table might look like:: - - session.query(Engineer).\ - filter(Engineer.id == Employee.id).\ - filter(Employee.name == 'dilbert').\ - update({"engineer_type": "programmer"}) - - * The polymorphic identity WHERE criteria is **not** included - for single- or - joined- table updates - this must be added **manually**, even - for single table inheritance. - - * The :meth:`.MapperEvents.before_update` and - :meth:`.MapperEvents.after_update` - events **are not invoked from this method**. Instead, the - :meth:`.SessionEvents.after_bulk_update` method is provided to - act upon a mass UPDATE of entity rows. .. seealso:: - :meth:`_query.Query.delete` + :ref:`bulk_update_delete` - :ref:`inserts_and_updates` - Core SQL tutorial """ @@ -3390,7 +3084,7 @@ class Query( """ stmt = self._statement_20(for_statement=for_statement, **kw) - assert for_statement == stmt.compile_options._for_statement + assert for_statement == stmt._compile_options._for_statement # this chooses between ORMFromStatementCompileState and # ORMSelectCompileState. We could also base this on @@ -3422,7 +3116,7 @@ class FromStatement(SelectStatementGrouping, Executable): __visit_name__ = "orm_from_statement" - compile_options = ORMFromStatementCompileState.default_compile_options + _compile_options = ORMFromStatementCompileState.default_compile_options _compile_state_factory = ORMFromStatementCompileState.create_for_statement diff --git a/lib/sqlalchemy/orm/session.py b/lib/sqlalchemy/orm/session.py index f4f7374e4..3d2f26e0d 100644 --- a/lib/sqlalchemy/orm/session.py +++ b/lib/sqlalchemy/orm/session.py @@ -30,6 +30,7 @@ from .unitofwork import UOWTransaction from .. import engine from .. import exc as sa_exc from .. import future +from .. import sql from .. import util from ..inspection import inspect from ..sql import coercions @@ -288,7 +289,7 @@ class ORMExecuteState(util.MemoizedSlots): return self.statement._execution_options.union(self._execution_options) def _orm_compile_options(self): - opts = self.statement.compile_options + opts = self.statement._compile_options if isinstance(opts, context.ORMCompileState.default_compile_options): return opts else: @@ -367,134 +368,53 @@ class ORMExecuteState(util.MemoizedSlots): class SessionTransaction(object): """A :class:`.Session`-level transaction. - :class:`.SessionTransaction` is a mostly behind-the-scenes object - not normally referenced directly by application code. It coordinates - among multiple :class:`_engine.Connection` objects, maintaining a database - transaction for each one individually, committing or rolling them - back all at once. It also provides optional two-phase commit behavior - which can augment this coordination operation. - - The :attr:`.Session.transaction` attribute of :class:`.Session` - refers to the current :class:`.SessionTransaction` object in use, if any. - The :attr:`.SessionTransaction.parent` attribute refers to the parent - :class:`.SessionTransaction` in the stack of :class:`.SessionTransaction` - objects. If this attribute is ``None``, then this is the top of the stack. - If non-``None``, then this :class:`.SessionTransaction` refers either - to a so-called "subtransaction" or a "nested" transaction. A - "subtransaction" is a scoping concept that demarcates an inner portion - of the outermost "real" transaction. A nested transaction, which - is indicated when the :attr:`.SessionTransaction.nested` - attribute is also True, indicates that this :class:`.SessionTransaction` - corresponds to a SAVEPOINT. - - **Life Cycle** - - A :class:`.SessionTransaction` is associated with a :class:`.Session` in - its default mode of ``autocommit=False`` whenever the "autobegin" process - takes place, associated with no database connections. As the - :class:`.Session` is called upon to emit SQL on behalf of various - :class:`_engine.Engine` or :class:`_engine.Connection` objects, - a corresponding - :class:`_engine.Connection` and associated :class:`.Transaction` - is added to a - collection within the :class:`.SessionTransaction` object, becoming one of - the connection/transaction pairs maintained by the - :class:`.SessionTransaction`. The start of a :class:`.SessionTransaction` - can be tracked using the :meth:`.SessionEvents.after_transaction_create` - event. - - The lifespan of the :class:`.SessionTransaction` ends when the - :meth:`.Session.commit`, :meth:`.Session.rollback` or - :meth:`.Session.close` methods are called. At this point, the - :class:`.SessionTransaction` removes its association with its parent - :class:`.Session`. A :class:`.Session` that is in ``autocommit=False`` - mode will create a new :class:`.SessionTransaction` to replace it when the - next "autobegin" event occurs, whereas a :class:`.Session` that's in - ``autocommit=True`` mode will remain without a :class:`.SessionTransaction` - until the :meth:`.Session.begin` method is called. The end of a - :class:`.SessionTransaction` can be tracked using the - :meth:`.SessionEvents.after_transaction_end` event. - - .. versionchanged:: 1.4 the :class:`.SessionTransaction` is not created - immediately within a :class:`.Session` when constructed or when the - previous transaction is removed, it instead is created when the - :class:`.Session` is next used. - - **Nesting and Subtransactions** - - Another detail of :class:`.SessionTransaction` behavior is that it is - capable of "nesting". This means that the :meth:`.Session.begin` method - can be called while an existing :class:`.SessionTransaction` is already - present, producing a new :class:`.SessionTransaction` that temporarily - replaces the parent :class:`.SessionTransaction`. When a - :class:`.SessionTransaction` is produced as nested, it assigns itself to - the :attr:`.Session.transaction` attribute, and it additionally will assign - the previous :class:`.SessionTransaction` to its :attr:`.Session.parent` - attribute. The behavior is effectively a - stack, where :attr:`.Session.transaction` refers to the current head of - the stack, and the :attr:`.SessionTransaction.parent` attribute allows - traversal up the stack until :attr:`.SessionTransaction.parent` is - ``None``, indicating the top of the stack. - - When the scope of :class:`.SessionTransaction` is ended via - :meth:`.Session.commit` or :meth:`.Session.rollback`, it restores its - parent :class:`.SessionTransaction` back onto the - :attr:`.Session.transaction` attribute. - - The purpose of this stack is to allow nesting of - :meth:`.Session.rollback` or :meth:`.Session.commit` calls in context - with various flavors of :meth:`.Session.begin`. This nesting behavior - applies to when :meth:`.Session.begin_nested` is used to emit a - SAVEPOINT transaction, and is also used to produce a so-called - "subtransaction" which allows a block of code to use a - begin/rollback/commit sequence regardless of whether or not its enclosing - code block has begun a transaction. The :meth:`.flush` method, whether - called explicitly or via autoflush, is the primary consumer of the - "subtransaction" feature, in that it wishes to guarantee that it works - within in a transaction block regardless of whether or not the - :class:`.Session` is in transactional mode when the method is called. - - Note that the flush process that occurs within the "autoflush" feature - as well as when the :meth:`.Session.flush` method is used **always** - creates a :class:`.SessionTransaction` object. This object is normally - a subtransaction, unless the :class:`.Session` is in autocommit mode - and no transaction exists at all, in which case it's the outermost - transaction. Any event-handling logic or other inspection logic - needs to take into account whether a :class:`.SessionTransaction` - is the outermost transaction, a subtransaction, or a "nested" / SAVEPOINT - transaction. + :class:`.SessionTransaction` is produced from the + :meth:`_orm.Session.begin` + and :meth:`_orm.Session.begin_nested` methods. It's largely an internal + object that in modern use provides a context manager for session + transactions. - .. seealso:: + Documentation on interacting with :class:`_orm.SessionTransaction` is + at: :ref:`unitofwork_transaction`. - :meth:`.Session.rollback` - :meth:`.Session.commit` + .. versionchanged:: 1.4 The scoping and API methods to work with the + :class:`_orm.SessionTransaction` object directly have been simplified. + + .. seealso:: + + :ref:`unitofwork_transaction` :meth:`.Session.begin` :meth:`.Session.begin_nested` - :attr:`.Session.is_active` + :meth:`.Session.rollback` + + :meth:`.Session.commit` - :meth:`.SessionEvents.after_transaction_create` + :meth:`.Session.in_transaction` - :meth:`.SessionEvents.after_transaction_end` + :meth:`.Session.in_nested_transaction` - :meth:`.SessionEvents.after_commit` + :meth:`.Session.get_transaction` - :meth:`.SessionEvents.after_rollback` + :meth:`.Session.get_nested_transaction` - :meth:`.SessionEvents.after_soft_rollback` """ _rollback_exception = None - def __init__(self, session, parent=None, nested=False, autobegin=False): + def __init__( + self, session, parent=None, nested=False, autobegin=False, + ): self.session = session self._connections = {} self._parent = parent self.nested = nested + if nested: + self._previous_nested_transaction = session._nested_transaction self._state = ACTIVE if not parent and nested: raise sa_exc.InvalidRequestError( @@ -688,6 +608,8 @@ class SessionTransaction(object): return self._connections[bind][0] local_connect = False + should_commit = True + if self._parent: conn = self._parent._connection_for_bind(bind, execution_options) if not self.nested: @@ -712,11 +634,16 @@ class SessionTransaction(object): transaction = conn.begin_twophase() elif self.nested: transaction = conn.begin_nested() - else: - if conn._is_future and conn.in_transaction(): - transaction = conn._transaction + elif conn.in_transaction(): + # if given a future connection already in a transaction, don't + # commit that transaction unless it is a savepoint + if conn.in_nested_transaction(): + transaction = conn.get_nested_transaction() else: - transaction = conn.begin() + transaction = conn.get_transaction() + should_commit = False + else: + transaction = conn.begin() except: # connection will not not be associated with this Session; # close it immediately so that it isn't closed under GC @@ -729,7 +656,7 @@ class SessionTransaction(object): self._connections[conn] = self._connections[conn.engine] = ( conn, transaction, - not bind_is_connection or not conn._is_future, + should_commit, not bind_is_connection, ) self.session.dispatch.after_begin(self.session, self, conn) @@ -748,7 +675,7 @@ class SessionTransaction(object): if self._parent is None or self.nested: self.session.dispatch.before_commit(self.session) - stx = self.session.transaction + stx = self.session._transaction if stx is not self: for subtransaction in stx._iterate_self_and_parents(upto=self): subtransaction.commit() @@ -775,7 +702,7 @@ class SessionTransaction(object): self._state = PREPARED - def commit(self): + def commit(self, _to_root=False): self._assert_active(prepared_ok=True) if self._state is not PREPARED: self._prepare_impl() @@ -793,12 +720,16 @@ class SessionTransaction(object): self._remove_snapshot() self.close() + + if _to_root and self._parent: + return self._parent.commit(_to_root=True) + return self._parent - def rollback(self, _capture_exception=False): + def rollback(self, _capture_exception=False, _to_root=False): self._assert_active(prepared_ok=True, rollback_ok=True) - stx = self.session.transaction + stx = self.session._transaction if stx is not self: for subtransaction in stx._iterate_self_and_parents(upto=self): subtransaction.close() @@ -849,20 +780,28 @@ class SessionTransaction(object): sess.dispatch.after_soft_rollback(sess, self) + if _to_root and self._parent: + return self._parent.rollback(_to_root=True) return self._parent def close(self, invalidate=False): + if self.nested: + self.session._nested_transaction = ( + self._previous_nested_transaction + ) + self.session._transaction = self._parent + if self._parent is None: for connection, transaction, should_commit, autoclose in set( self._connections.values() ): if invalidate: connection.invalidate() + if should_commit and transaction.is_active: + transaction.close() if autoclose: connection.close() - else: - transaction.close() self._state = CLOSED self.session.dispatch.after_transaction_end(self.session, self) @@ -924,6 +863,15 @@ class Session(_SessionClassMethods): "scalar", ) + @util.deprecated_params( + autocommit=( + "2.0", + "The :paramref:`.Session.autocommit` parameter is deprecated " + "and will be removed in SQLAlchemy version 2.0. Please use the " + ":paramref:`.Session.autobegin` parameter set to False to support " + "explicit use of the :meth:`.Session.begin` method.", + ), + ) def __init__( self, bind=None, @@ -1071,8 +1019,6 @@ class Session(_SessionClassMethods): :class:`.Session` dictionary will be local to that :class:`.Session`. - .. versionadded:: 0.9.0 - :param query_cls: Class which should be used to create new Query objects, as returned by the :meth:`~.Session.query` method. Defaults to :class:`_query.Query`. @@ -1096,13 +1042,23 @@ class Session(_SessionClassMethods): self._flushing = False self._warn_on_events = False self._transaction = None + self._nested_transaction = None self.future = future self.hash_key = _new_sessionid() self.autoflush = autoflush - self.autocommit = autocommit self.expire_on_commit = expire_on_commit self.enable_baked_queries = enable_baked_queries + if autocommit: + if future: + raise sa_exc.ArgumentError( + "Cannot use autocommit mode with future=True. " + "use the autobegin flag." + ) + self.autocommit = True + else: + self.autocommit = False + self.twophase = twophase self._query_cls = query_cls if query_cls else query.Query if info: @@ -1116,21 +1072,77 @@ class Session(_SessionClassMethods): connection_callable = None + def __enter__(self): + return self + + def __exit__(self, type_, value, traceback): + self.close() + @property + @util.deprecated_20( + "The :attr:`_orm.Session.transaction` accessor is deprecated and " + "will be removed in SQLAlchemy version 2.0. " + "For context manager use, use :meth:`_orm.Session.begin`. To access " + "the current root transaction, use " + ":meth:`_orm.Session.get_transaction()" + ) def transaction(self): """The current active or inactive :class:`.SessionTransaction`. - If this session is in "autobegin" mode and the transaction was not - begun, this accessor will implicitly begin the transaction. + May be None if no transaction has begun yet. .. versionchanged:: 1.4 the :attr:`.Session.transaction` attribute - is now a read-only descriptor that will automatically start a - transaction in "autobegin" mode if one is not present. + is now a read-only descriptor that also may return None if no + transaction has begun yet. + """ - self._autobegin() + if not self.future: + self._autobegin() return self._transaction + def in_transaction(self): + """Return True if this :class:`_orm.Session` has begun a transaction. + + .. versionadded:: 1.4 + + .. seealso:: + + :attr:`_orm.Session.is_active` + + + """ + return self._transaction is not None + + def in_nested_transaction(self): + """Return True if this :class:`_orm.Session` has begun a nested + transaction, e.g. SAVEPOINT. + + .. versionadded:: 1.4 + + """ + return self._nested_transaction is not None + + def get_transaction(self): + """Return the current root transaction in progress, if any. + + .. versionadded:: 1.4 + + """ + trans = self._transaction + while trans is not None and trans._parent is not None: + trans = trans._parent + return trans + + def get_nested_transaction(self): + """Return the current nested transaction in progress, if any. + + .. versionadded:: 1.4 + + """ + + return self._nested_transaction + @util.memoized_property def info(self): """A user-modifiable dictionary. @@ -1141,8 +1153,6 @@ class Session(_SessionClassMethods): here is always local to this :class:`.Session` and can be modified independently of all other :class:`.Session` objects. - .. versionadded:: 0.9.0 - """ return {} @@ -1153,7 +1163,17 @@ class Session(_SessionClassMethods): return False - def begin(self, subtransactions=False, nested=False): + @util.deprecated_params( + subtransactions=( + "2.0", + "The :paramref:`_orm.Session.begin.subtransactions` flag is " + "deprecated and " + "will be removed in SQLAlchemy version 2.0. The " + ":attr:`_orm.Session.transaction` flag may " + "be checked for None before invoking :meth:`_orm.Session.begin`.", + ) + ) + def begin(self, subtransactions=False, nested=False, _subtrans=False): """Begin a transaction on this :class:`.Session`. .. warning:: @@ -1206,17 +1226,24 @@ class Session(_SessionClassMethods): """ + if subtransactions and self.future: + raise NotImplementedError( + "subtransactions are not implemented in future " + "Session objects." + ) if self._autobegin(): if not subtransactions and not nested: - return + return self._transaction if self._transaction is not None: - if subtransactions or nested: - self._transaction = self._transaction._begin(nested=nested) + if subtransactions or _subtrans or nested: + trans = self._transaction._begin(nested=nested) + self._transaction = trans + if nested: + self._nested_transaction = trans else: raise sa_exc.InvalidRequestError( - "A transaction is already begun. Use " - "subtransactions=True to allow subtransactions." + "A transaction is already begun on this Session." ) else: self._transaction = SessionTransaction(self, nested=nested) @@ -1265,7 +1292,7 @@ class Session(_SessionClassMethods): if self._transaction is None: pass else: - self._transaction.rollback() + self._transaction.rollback(_to_root=self.future) def commit(self): """Flush pending changes and commit the current transaction. @@ -1299,7 +1326,7 @@ class Session(_SessionClassMethods): if not self._autobegin(): raise sa_exc.InvalidRequestError("No transaction is begun.") - self._transaction.commit() + self._transaction.commit(_to_root=self.future) def prepare(self): """Prepare the current transaction in progress for two phase commit. @@ -1371,8 +1398,6 @@ class Session(_SessionClassMethods): present within the :class:`.Session`, a warning is emitted and the arguments are ignored. - .. versionadded:: 0.9.9 - .. seealso:: :ref:`session_transaction_isolation` @@ -1402,6 +1427,7 @@ class Session(_SessionClassMethods): ) assert self._transaction is None + assert self.autocommit conn = engine.connect(**kw) if execution_options: conn = conn.execution_options(**execution_options) @@ -1663,12 +1689,19 @@ class Session(_SessionClassMethods): This is a variant of :meth:`.Session.close` that will additionally ensure that the :meth:`_engine.Connection.invalidate` - method will be called - on all :class:`_engine.Connection` objects. This can be called when - the database is known to be in a state where the connections are - no longer safe to be used. + method will be called on each :class:`_engine.Connection` object + that is currently in use for a transaction (typically there is only + one connection unless the :class:`_orm.Session` is used with + multiple engines). - E.g.:: + This can be called when the database is known to be in a state where + the connections are no longer safe to be used. + + Below illustrates a scenario when using `gevent + <http://www.gevent.org/>`_, which can produce ``Timeout`` exceptions + that may mean the underlying connection should be discarded:: + + import gevent try: sess = Session() @@ -1681,13 +1714,8 @@ class Session(_SessionClassMethods): sess.rollback() raise - This clears all items and ends any transaction in progress. - - If this session were created with ``autocommit=False``, a new - transaction is immediately begun. Note that this new transaction does - not use any connection resources until they are first needed. - - .. versionadded:: 0.9.9 + The method additionally does everything that :meth:`_orm.Session.close` + does, including that all ORM objects are expunged. """ self._close_impl(invalidate=True) @@ -2118,13 +2146,7 @@ class Session(_SessionClassMethods): "A blank dictionary is ambiguous." ) - if with_for_update is not None: - if with_for_update is True: - with_for_update = query.ForUpdateArg() - elif with_for_update: - with_for_update = query.ForUpdateArg(**with_for_update) - else: - with_for_update = None + with_for_update = query.ForUpdateArg._from_argument(with_for_update) stmt = future.select(object_mapper(instance)) if ( @@ -2482,6 +2504,200 @@ class Session(_SessionClassMethods): for o, m, st_, dct_ in cascade_states: self._delete_impl(st_, o, False) + def get( + self, + entity, + ident, + options=None, + populate_existing=False, + with_for_update=None, + identity_token=None, + ): + """Return an instance based on the given primary key identifier, + or ``None`` if not found. + + E.g.:: + + my_user = session.get(User, 5) + + some_object = session.get(VersionedFoo, (5, 10)) + + some_object = session.get( + VersionedFoo, + {"id": 5, "version_id": 10} + ) + + .. versionadded:: 1.4 Added :meth:`_orm.Session.get`, which is moved + from the now deprecated :meth:`_orm.Query.get` method. + + :meth:`_orm.Session.get` is special in that it provides direct + access to the identity map of the :class:`.Session`. + If the given primary key identifier is present + in the local identity map, the object is returned + directly from this collection and no SQL is emitted, + unless the object has been marked fully expired. + If not present, + a SELECT is performed in order to locate the object. + + :meth:`_orm.Session.get` also will perform a check if + the object is present in the identity map and + marked as expired - a SELECT + is emitted to refresh the object as well as to + ensure that the row is still present. + If not, :class:`~sqlalchemy.orm.exc.ObjectDeletedError` is raised. + + :param entity: a mapped class or :class:`.Mapper` indicating the + type of entity to be loaded. + + :param ident: A scalar, tuple, or dictionary representing the + primary key. For a composite (e.g. multiple column) primary key, + a tuple or dictionary should be passed. + + For a single-column primary key, the scalar calling form is typically + the most expedient. If the primary key of a row is the value "5", + the call looks like:: + + my_object = session.get(SomeClass, 5) + + The tuple form contains primary key values typically in + the order in which they correspond to the mapped + :class:`_schema.Table` + object's primary key columns, or if the + :paramref:`_orm.Mapper.primary_key` configuration parameter were + used, in + the order used for that parameter. For example, if the primary key + of a row is represented by the integer + digits "5, 10" the call would look like:: + + my_object = session.get(SomeClass, (5, 10)) + + The dictionary form should include as keys the mapped attribute names + corresponding to each element of the primary key. If the mapped class + has the attributes ``id``, ``version_id`` as the attributes which + store the object's primary key value, the call would look like:: + + my_object = session.get(SomeClass, {"id": 5, "version_id": 10}) + + :param options: optional sequence of loader options which will be + applied to the query, if one is emitted. + + :param populate_existing: causes the method to unconditionally emit + a SQL query and refresh the object with the newly loaded data, + regardless of whether or not the object is already present. + + :param with_for_update: optional boolean ``True`` indicating FOR UPDATE + should be used, or may be a dictionary containing flags to + indicate a more specific set of FOR UPDATE flags for the SELECT; + flags should match the parameters of + :meth:`_query.Query.with_for_update`. + Supersedes the :paramref:`.Session.refresh.lockmode` parameter. + + :return: The object instance, or ``None``. + + """ + return self._get_impl( + entity, + ident, + loading.load_on_pk_identity, + options, + populate_existing=populate_existing, + with_for_update=with_for_update, + identity_token=identity_token, + ) + + def _get_impl( + self, + entity, + primary_key_identity, + db_load_fn, + options=None, + populate_existing=False, + with_for_update=None, + identity_token=None, + execution_options=None, + ): + + # convert composite types to individual args + if hasattr(primary_key_identity, "__composite_values__"): + primary_key_identity = primary_key_identity.__composite_values__() + + mapper = inspect(entity) + + is_dict = isinstance(primary_key_identity, dict) + if not is_dict: + primary_key_identity = util.to_list( + primary_key_identity, default=(None,) + ) + + if len(primary_key_identity) != len(mapper.primary_key): + raise sa_exc.InvalidRequestError( + "Incorrect number of values in identifier to formulate " + "primary key for query.get(); primary key columns are %s" + % ",".join("'%s'" % c for c in mapper.primary_key) + ) + + if is_dict: + try: + primary_key_identity = list( + primary_key_identity[prop.key] + for prop in mapper._identity_key_props + ) + + except KeyError as err: + util.raise_( + sa_exc.InvalidRequestError( + "Incorrect names of values in identifier to formulate " + "primary key for query.get(); primary key attribute " + "names are %s" + % ",".join( + "'%s'" % prop.key + for prop in mapper._identity_key_props + ) + ), + replace_context=err, + ) + + if ( + not populate_existing + and not mapper.always_refresh + and with_for_update is None + ): + + instance = self._identity_lookup( + mapper, primary_key_identity, identity_token=identity_token + ) + + if instance is not None: + # reject calls for id in identity map but class + # mismatch. + if not issubclass(instance.__class__, mapper.class_): + return None + return instance + elif instance is attributes.PASSIVE_CLASS_MISMATCH: + return None + + # apply_labels() not strictly necessary, however this will ensure that + # tablename_colname style is used which at the moment is asserted + # in a lot of unit tests :) + + load_options = context.QueryContext.default_load_options + + if populate_existing: + load_options += {"_populate_existing": populate_existing} + statement = sql.select(mapper).apply_labels() + if with_for_update is not None: + statement._for_update_arg = query.ForUpdateArg._from_argument( + with_for_update + ) + + if options: + statement = statement.options(*options) + if execution_options: + statement = statement.execution_options(**execution_options) + return db_load_fn( + self, statement, primary_key_identity, load_options=load_options, + ) + def merge(self, instance, load=True): """Copy the state of a given instance into a corresponding instance within this :class:`.Session`. @@ -2629,7 +2845,7 @@ class Session(_SessionClassMethods): new_instance = True elif key_is_persistent: - merged = self.query(mapper.class_).get(key[1]) + merged = self.get(mapper.class_, key[1], identity_token=key[2]) if merged is None: merged = mapper.class_manager.new_instance() @@ -3021,9 +3237,7 @@ class Session(_SessionClassMethods): if not flush_context.has_work: return - flush_context.transaction = transaction = self.begin( - subtransactions=True - ) + flush_context.transaction = transaction = self.begin(_subtrans=True) try: self._warn_on_events = True try: @@ -3338,7 +3552,7 @@ class Session(_SessionClassMethods): mapper = _class_to_mapper(mapper) self._flushing = True - transaction = self.begin(subtransactions=True) + transaction = self.begin(_subtrans=True) try: if isupdate: persistence._bulk_update( @@ -3441,62 +3655,38 @@ class Session(_SessionClassMethods): @property def is_active(self): - """True if this :class:`.Session` is in "transaction mode" and - is not in "partial rollback" state. - - The :class:`.Session` in its default mode of ``autocommit=False`` - is essentially always in "transaction mode", in that a - :class:`.SessionTransaction` is associated with it as soon as - it is instantiated. This :class:`.SessionTransaction` is immediately - replaced with a new one as soon as it is ended, due to a rollback, - commit, or close operation. - - "Transaction mode" does *not* indicate whether - or not actual database connection resources are in use; the - :class:`.SessionTransaction` object coordinates among zero or more - actual database transactions, and starts out with none, accumulating - individual DBAPI connections as different data sources are used - within its scope. The best way to track when a particular - :class:`.Session` has actually begun to use DBAPI resources is to - implement a listener using the :meth:`.SessionEvents.after_begin` - method, which will deliver both the :class:`.Session` as well as the - target :class:`_engine.Connection` to a user-defined event listener. - - The "partial rollback" state refers to when an "inner" transaction, - typically used during a flush, encounters an error and emits a - rollback of the DBAPI connection. At this point, the - :class:`.Session` is in "partial rollback" and awaits for the user to - call :meth:`.Session.rollback`, in order to close out the - transaction stack. It is in this "partial rollback" period that the - :attr:`.is_active` flag returns False. After the call to - :meth:`.Session.rollback`, the :class:`.SessionTransaction` is - replaced with a new one and :attr:`.is_active` returns ``True`` again. - - When a :class:`.Session` is used in ``autocommit=True`` mode, the - :class:`.SessionTransaction` is only instantiated within the scope - of a flush call, or when :meth:`.Session.begin` is called. So - :attr:`.is_active` will always be ``False`` outside of a flush or - :meth:`.Session.begin` block in this mode, and will be ``True`` - within the :meth:`.Session.begin` block as long as it doesn't enter - "partial rollback" state. - - From all the above, it follows that the only purpose to this flag is - for application frameworks that wish to detect if a "rollback" is - necessary within a generic error handling routine, for - :class:`.Session` objects that would otherwise be in - "partial rollback" mode. In a typical integration case, this is also - not necessary as it is standard practice to emit - :meth:`.Session.rollback` unconditionally within the outermost - exception catch. - - To track the transactional state of a :class:`.Session` fully, - use event listeners, primarily the :meth:`.SessionEvents.after_begin`, - :meth:`.SessionEvents.after_commit`, - :meth:`.SessionEvents.after_rollback` and related events. + """True if this :class:`.Session` not in "partial rollback" state. + + .. versionchanged:: 1.4 The :class:`_orm.Session` no longer begins + a new transaction immediately, so this attribute will be False + when the :class:`_orm.Session` is first instantiated. + + "partial rollback" state typically indicates that the flush process + of the :class:`_orm.Session` has failed, and that the + :meth:`_orm.Session.rollback` method must be emitted in order to + fully roll back the transaction. + + If this :class:`_orm.Session` is not in a transaction at all, the + :class:`_orm.Session` will autobegin when it is first used, so in this + case :attr:`_orm.Session.is_active` will return True. + + Otherwise, if this :class:`_orm.Session` is within a transaction, + and that transaction has not been rolled back internally, the + :attr:`_orm.Session.is_active` will also return True. + + .. seealso:: + + :ref:`faq_session_rollback` + + :meth:`_orm.Session.in_transaction` """ - self._autobegin() - return self._transaction and self._transaction.is_active + if self.autocommit: + return ( + self._transaction is not None and self._transaction.is_active + ) + else: + return self._transaction is None or self._transaction.is_active identity_map = None """A mapping of object identities to objects themselves. @@ -3576,36 +3766,84 @@ class sessionmaker(_SessionClassMethods): e.g.:: - # global scope - Session = sessionmaker(autoflush=False) + from sqlalchemy import create_engine + from sqlalchemy.orm import sessionmaker - # later, in a local scope, create and use a session: - sess = Session() + # an Engine, which the Session will use for connection + # resources + engine = create_engine('postgresql://scott:tiger@localhost/') - Any keyword arguments sent to the constructor itself will override the - "configured" keywords:: + Session = sessionmaker(engine) - Session = sessionmaker() + with Session() as session: + session.add(some_object) + session.add(some_other_object) + session.commit() + + Context manager use is optional; otherwise, the returned + :class:`_orm.Session` object may be closed explicitly via the + :meth:`_orm.Session.close` method. Using a + ``try:/finally:`` block is optional, however will ensure that the close + takes place even if there are database errors:: + + session = Session() + try: + session.add(some_object) + session.add(some_other_object) + session.commit() + finally: + session.close() + + :class:`.sessionmaker` acts as a factory for :class:`_orm.Session` + objects in the same way as an :class:`_engine.Engine` acts as a factory + for :class:`_engine.Connection` objects. In this way it also includes + a :meth:`_orm.sessionmaker.begin` method, that provides a context + manager which both begins and commits a transaction, as well as closes + out the :class:`_orm.Session` when complete, rolling back the transaction + if any errors occur:: + + Session = sessionmaker(engine) + + wih Session.begin() as session: + session.add(some_object) + session.add(some_other_object) + # commits transaction, closes session + + .. versionadded:: 1.4 + + When calling upon :class:`_orm.sessionmaker` to construct a + :class:`_orm.Session`, keyword arguments may also be passed to the + method; these arguments will override that of the globally configured + parameters. Below we use a :class:`_orm.sessionmaker` bound to a certain + :class:`_engine.Engine` to produce a :class:`_orm.Session` that is instead + bound to a specific :class:`_engine.Connection` procured from that engine:: + + Session = sessionmaker(engine) # bind an individual session to a connection - sess = Session(bind=connection) - The class also includes a method :meth:`.configure`, which can - be used to specify additional keyword arguments to the factory, which - will take effect for subsequent :class:`.Session` objects generated. - This is usually used to associate one or more :class:`_engine.Engine` - objects - with an existing :class:`.sessionmaker` factory before it is first - used:: + with engine.connect() as connection: + with Session(bind=connection) as session: + # work with session + + The class also includes a method :meth:`_orm.sessionmaker.configure`, which + can be used to specify additional keyword arguments to the factory, which + will take effect for subsequent :class:`.Session` objects generated. This + is usually used to associate one or more :class:`_engine.Engine` objects + with an existing + :class:`.sessionmaker` factory before it is first used:: - # application starts + # application starts, sessionmaker does not have + # an engine bound yet Session = sessionmaker() - # ... later + # ... later, when an engine URL is read from a configuration + # file or other events allow the engine to be created engine = create_engine('sqlite:///foo.db') Session.configure(bind=engine) sess = Session() + # work with session .. seealso:: @@ -3646,8 +3884,6 @@ class sessionmaker(_SessionClassMethods): replaced, when the ``info`` parameter is specified to the specific :class:`.Session` construction operation. - .. versionadded:: 0.9.0 - :param \**kw: all other keyword arguments are passed to the constructor of newly created :class:`.Session` objects. @@ -3663,6 +3899,29 @@ class sessionmaker(_SessionClassMethods): # events can be associated with it specifically. self.class_ = type(class_.__name__, (class_,), {}) + @util.contextmanager + def begin(self): + """Produce a context manager that both provides a new + :class:`_orm.Session` as well as a transaction that commits. + + + e.g.:: + + Session = sessionmaker(some_engine) + + with Session.begin() as session: + session.add(some_object) + + # commits transaction, closes session + + .. versionadded:: 1.4 + + + """ + with self() as session: + with session.begin(): + yield session + def __call__(self, **local_kw): """Produce a new :class:`.Session` object using the configuration established in this :class:`.sessionmaker`. @@ -3806,8 +4065,6 @@ def make_transient_to_detached(instance): call to :meth:`.Session.merge` in that a given persistent state can be manufactured without any SQL calls. - .. versionadded:: 0.9.5 - .. seealso:: :func:`.make_transient` diff --git a/lib/sqlalchemy/orm/strategies.py b/lib/sqlalchemy/orm/strategies.py index 53cc99ccd..db82f0b74 100644 --- a/lib/sqlalchemy/orm/strategies.py +++ b/lib/sqlalchemy/orm/strategies.py @@ -677,7 +677,7 @@ class LazyLoader(AbstractRelationshipLoader, util.MemoizedSlots): self._equated_columns[c] = self._equated_columns[col] self.logger.info( - "%s will use query.get() to " "optimize instance loads", self + "%s will use Session.get() to " "optimize instance loads", self ) def init_class_attribute(self, mapper): diff --git a/lib/sqlalchemy/orm/util.py b/lib/sqlalchemy/orm/util.py index e04c54497..68ffa2393 100644 --- a/lib/sqlalchemy/orm/util.py +++ b/lib/sqlalchemy/orm/util.py @@ -1621,3 +1621,108 @@ def randomize_unitofwork(): topological.set = ( unitofwork.set ) = session.set = mapper.set = dependency.set = RandomSet + + +def _offset_or_limit_clause(element, name=None, type_=None): + """Convert the given value to an "offset or limit" clause. + + This handles incoming integers and converts to an expression; if + an expression is already given, it is passed through. + + """ + return coercions.expect( + roles.LimitOffsetRole, element, name=name, type_=type_ + ) + + +def _offset_or_limit_clause_asint_if_possible(clause): + """Return the offset or limit clause as a simple integer if possible, + else return the clause. + + """ + if clause is None: + return None + if hasattr(clause, "_limit_offset_value"): + value = clause._limit_offset_value + return util.asint(value) + else: + return clause + + +def _make_slice(limit_clause, offset_clause, start, stop): + """Compute LIMIT/OFFSET in terms of slice start/end + """ + + # for calculated limit/offset, try to do the addition of + # values to offset in Python, however if a SQL clause is present + # then the addition has to be on the SQL side. + if start is not None and stop is not None: + offset_clause = _offset_or_limit_clause_asint_if_possible( + offset_clause + ) + if offset_clause is None: + offset_clause = 0 + + if start != 0: + offset_clause = offset_clause + start + + if offset_clause == 0: + offset_clause = None + else: + offset_clause = _offset_or_limit_clause(offset_clause) + + limit_clause = _offset_or_limit_clause(stop - start) + + elif start is None and stop is not None: + limit_clause = _offset_or_limit_clause(stop) + elif start is not None and stop is None: + offset_clause = _offset_or_limit_clause_asint_if_possible( + offset_clause + ) + if offset_clause is None: + offset_clause = 0 + + if start != 0: + offset_clause = offset_clause + start + + if offset_clause == 0: + offset_clause = None + else: + offset_clause = _offset_or_limit_clause(offset_clause) + + return limit_clause, offset_clause + + +def _getitem(iterable_query, item): + """calculate __getitem__ in terms of an iterable query object + that also has a slice() method. + + """ + + if isinstance(item, slice): + start, stop, step = util.decode_slice(item) + + if ( + isinstance(stop, int) + and isinstance(start, int) + and stop - start <= 0 + ): + return [] + + # perhaps we should execute a count() here so that we + # can still use LIMIT/OFFSET ? + elif (isinstance(start, int) and start < 0) or ( + isinstance(stop, int) and stop < 0 + ): + return list(iterable_query)[item] + + res = iterable_query.slice(start, stop) + if step is not None: + return list(res)[None : None : item.step] + else: + return list(res) + else: + if item == -1: + return list(iterable_query)[-1] + else: + return list(iterable_query[item : item + 1])[0] diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py index 4bc6d8280..36a8151d3 100644 --- a/lib/sqlalchemy/sql/base.py +++ b/lib/sqlalchemy/sql/base.py @@ -571,6 +571,26 @@ class Options(util.with_metaclass(_MetaOptions)): o1.__dict__.update(other) return o1 + def __eq__(self, other): + # TODO: very inefficient. This is used only in test suites + # right now. + for a, b in util.zip_longest(self._cache_attrs, other._cache_attrs): + if getattr(self, a) != getattr(other, b): + return False + return True + + def __repr__(self): + # TODO: fairly inefficient, used only in debugging right now. + + return "%s(%s)" % ( + self.__class__.__name__, + ", ".join( + "%s=%r" % (k, self.__dict__[k]) + for k in self._cache_attrs + if k in self.__dict__ + ), + ) + @hybridmethod def add_to_element(self, name, value): return self + {name: getattr(self, name) + value} @@ -610,6 +630,60 @@ class Options(util.with_metaclass(_MetaOptions)): ) return cls + d + @classmethod + def from_execution_options( + cls, key, attrs, exec_options, statement_exec_options + ): + """"process Options argument in terms of execution options. + + + e.g.:: + + ( + load_options, + execution_options, + ) = QueryContext.default_load_options.from_execution_options( + "_sa_orm_load_options", + { + "populate_existing", + "autoflush", + "yield_per" + }, + execution_options, + statement._execution_options, + ) + + get back the Options and refresh "_sa_orm_load_options" in the + exec options dict w/ the Options as well + + """ + + # common case is that no options we are looking for are + # in either dictionary, so cancel for that first + check_argnames = attrs.intersection( + set(exec_options).union(statement_exec_options) + ) + + existing_options = exec_options.get(key, cls) + + if check_argnames: + result = {} + for argname in check_argnames: + local = "_" + argname + if argname in exec_options: + result[local] = exec_options[argname] + elif argname in statement_exec_options: + result[local] = statement_exec_options[argname] + + new_options = existing_options + result + exec_options = util.immutabledict().merge_with( + exec_options, {key: new_options} + ) + return new_options, exec_options + + else: + return existing_options, exec_options + class CacheableOptions(Options, HasCacheKey): @hybridmethod diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py index be412c770..588c485ae 100644 --- a/lib/sqlalchemy/sql/coercions.py +++ b/lib/sqlalchemy/sql/coercions.py @@ -52,6 +52,18 @@ def _document_text_coercion(paramname, meth_rst, param_rst): ) +def _expression_collection_was_a_list(attrname, fnname, args): + if args and isinstance(args[0], (list, set)) and len(args) == 1: + util.warn_deprecated_20( + 'The "%s" argument to %s() is now passed as a series of ' + "positional " + "elements, rather than as a list. " % (attrname, fnname) + ) + return args[0] + else: + return args + + def expect(role, element, apply_propagate_attrs=None, argname=None, **kw): if ( role.allows_lambda diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 6ce505412..c7e5aabcc 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -2573,10 +2573,8 @@ class Case(ColumnElement): stmt = select([users_table]).\ where( case( - [ - (users_table.c.name == 'wendy', 'W'), - (users_table.c.name == 'jack', 'J') - ], + (users_table.c.name == 'wendy', 'W'), + (users_table.c.name == 'jack', 'J'), else_='E' ) ) @@ -2597,7 +2595,10 @@ class Case(ColumnElement): ("else_", InternalTraversal.dp_clauseelement), ] - def __init__(self, whens, value=None, else_=None): + # TODO: for Py2k removal, this will be: + # def __init__(self, *whens, value=None, else_=None): + + def __init__(self, *whens, **kw): r"""Produce a ``CASE`` expression. The ``CASE`` construct in SQL is a conditional object that @@ -2612,10 +2613,8 @@ class Case(ColumnElement): stmt = select([users_table]).\ where( case( - [ - (users_table.c.name == 'wendy', 'W'), - (users_table.c.name == 'jack', 'J') - ], + (users_table.c.name == 'wendy', 'W'), + (users_table.c.name == 'jack', 'J'), else_='E' ) ) @@ -2660,16 +2659,14 @@ class Case(ColumnElement): from sqlalchemy import case, literal_column case( - [ - ( - orderline.c.qty > 100, - literal_column("'greaterthan100'") - ), - ( - orderline.c.qty > 10, - literal_column("'greaterthan10'") - ) - ], + ( + orderline.c.qty > 100, + literal_column("'greaterthan100'") + ), + ( + orderline.c.qty > 10, + literal_column("'greaterthan10'") + ), else_=literal_column("'lessthan10'") ) @@ -2683,19 +2680,23 @@ class Case(ColumnElement): ELSE 'lessthan10' END - :param whens: The criteria to be compared against, + :param \*whens: The criteria to be compared against, :paramref:`.case.whens` accepts two different forms, based on whether or not :paramref:`.case.value` is used. + .. versionchanged:: 1.4 the :func:`_sql.case` + function now accepts the series of WHEN conditions positionally; + passing the expressions within a list is deprecated. + In the first form, it accepts a list of 2-tuples; each 2-tuple consists of ``(<sql expression>, <value>)``, where the SQL expression is a boolean expression and "value" is a resulting value, e.g.:: - case([ + case( (users_table.c.name == 'wendy', 'W'), (users_table.c.name == 'jack', 'J') - ]) + ) In the second form, it accepts a Python dictionary of comparison values mapped to a resulting value; this form requires @@ -2720,11 +2721,23 @@ class Case(ColumnElement): """ + if "whens" in kw: + util.warn_deprecated_20( + 'The "whens" argument to case() is now passed as a series of ' + "positional " + "elements, rather than as a list. " + ) + whens = kw.pop("whens") + else: + whens = coercions._expression_collection_was_a_list( + "whens", "case", whens + ) try: whens = util.dictlike_iteritems(whens) except TypeError: pass + value = kw.pop("value", None) if value is not None: whenlist = [ ( @@ -2760,11 +2773,16 @@ class Case(ColumnElement): self.type = type_ self.whens = whenlist + + else_ = kw.pop("else_", None) if else_ is not None: self.else_ = coercions.expect(roles.ExpressionElementRole, else_) else: self.else_ = None + if kw: + raise TypeError("unknown arguments: %s" % (", ".join(sorted(kw)))) + @property def _from_objects(self): return list( diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index 85db88345..2d369cdf8 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -169,9 +169,6 @@ class Operators(object): :class:`.Boolean`, and those that do not will be of the same type as the left-hand operand. - .. versionadded:: 1.2.0b3 - added the - :paramref:`.Operators.op.return_type` argument. - .. seealso:: :ref:`types_operators` @@ -194,8 +191,6 @@ class Operators(object): :paramref:`.Operators.op.is_comparison` flag with True. - .. versionadded:: 1.2.0b3 - .. seealso:: :meth:`.Operators.op` @@ -723,15 +718,6 @@ class ColumnOperators(Operators): With the value of ``:param`` as ``"foo/%bar"``. - .. versionadded:: 1.2 - - .. versionchanged:: 1.2.0 The - :paramref:`.ColumnOperators.startswith.autoescape` parameter is - now a simple boolean rather than a character; the escape - character itself is also escaped, and defaults to a forwards - slash, which itself can be customized using the - :paramref:`.ColumnOperators.startswith.escape` parameter. - :param escape: a character which when given will render with the ``ESCAPE`` keyword to establish that character as the escape character. This character can then be placed preceding occurrences @@ -811,15 +797,6 @@ class ColumnOperators(Operators): With the value of ``:param`` as ``"foo/%bar"``. - .. versionadded:: 1.2 - - .. versionchanged:: 1.2.0 The - :paramref:`.ColumnOperators.endswith.autoescape` parameter is - now a simple boolean rather than a character; the escape - character itself is also escaped, and defaults to a forwards - slash, which itself can be customized using the - :paramref:`.ColumnOperators.endswith.escape` parameter. - :param escape: a character which when given will render with the ``ESCAPE`` keyword to establish that character as the escape character. This character can then be placed preceding occurrences @@ -899,15 +876,6 @@ class ColumnOperators(Operators): With the value of ``:param`` as ``"foo/%bar"``. - .. versionadded:: 1.2 - - .. versionchanged:: 1.2.0 The - :paramref:`.ColumnOperators.contains.autoescape` parameter is - now a simple boolean rather than a character; the escape - character itself is also escaped, and defaults to a forwards - slash, which itself can be customized using the - :paramref:`.ColumnOperators.contains.escape` parameter. - :param escape: a character which when given will render with the ``ESCAPE`` keyword to establish that character as the escape character. This character can then be placed preceding occurrences diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 12fcc00c3..1155c273b 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -2225,6 +2225,17 @@ class ForUpdateArg(ClauseElement): ("skip_locked", InternalTraversal.dp_boolean), ] + @classmethod + def _from_argument(cls, with_for_update): + if isinstance(with_for_update, ForUpdateArg): + return with_for_update + elif with_for_update in (None, False): + return None + elif with_for_update is True: + return ForUpdateArg() + else: + return ForUpdateArg(**with_for_update) + def __eq__(self, other): return ( isinstance(other, ForUpdateArg) @@ -2699,6 +2710,12 @@ class SelectStatementGrouping(GroupedElement, SelectBase): class DeprecatedSelectBaseGenerations(object): + """A collection of methods available on :class:`_sql.Select` and + :class:`_sql.CompoundSelect`, these are all **deprecated** methods as they + modify the object in-place. + + """ + @util.deprecated( "1.4", "The :meth:`_expression.GenerativeSelect.append_order_by` " @@ -2740,9 +2757,6 @@ class DeprecatedSelectBaseGenerations(object): as it provides standard :term:`method chaining`. - .. seealso:: - - :meth:`_expression.GenerativeSelect.group_by` """ self.group_by.non_generative(self, *clauses) @@ -3353,6 +3367,12 @@ class CompoundSelect(HasCompileState, GenerativeSelect): class DeprecatedSelectGenerations(object): + """A collection of methods available on :class:`_sql.Select`, these + are all **deprecated** methods as they modify the :class:`_sql.Select` + object in -place. + + """ + @util.deprecated( "1.4", "The :meth:`_expression.Select.append_correlation` " @@ -3377,7 +3397,7 @@ class DeprecatedSelectGenerations(object): "1.4", "The :meth:`_expression.Select.append_column` method is deprecated " "and will be removed in a future release. Use the generative " - "method :meth:`_expression.Select.column`.", + "method :meth:`_expression.Select.add_columns`.", ) def append_column(self, column): """Append the given column expression to the columns clause of this @@ -3388,14 +3408,10 @@ class DeprecatedSelectGenerations(object): my_select.append_column(some_table.c.new_column) This is an **in-place** mutation method; the - :meth:`_expression.Select.column` method is preferred, + :meth:`_expression.Select.add_columns` method is preferred, as it provides standard :term:`method chaining`. - See the documentation for :meth:`_expression.Select.with_only_columns` - for guidelines on adding /replacing the columns of a - :class:`_expression.Select` object. - """ self.add_columns.non_generative(self, column) @@ -3501,6 +3517,21 @@ class SelectState(util.MemoizedSlots, CompileState): self.columns_plus_names = statement._generate_columns_plus_names(True) + @classmethod + def _plugin_not_implemented(cls): + raise NotImplementedError( + "The default SELECT construct without plugins does not " + "implement this method." + ) + + @classmethod + def get_column_descriptions(cls, statement): + cls._plugin_not_implemented() + + @classmethod + def from_statement(cls, statement, from_statement): + cls._plugin_not_implemented() + def _get_froms(self, statement): seen = set() froms = [] @@ -3805,6 +3836,15 @@ class Select( ): """Represents a ``SELECT`` statement. + The :class:`_sql.Select` object is normally constructed using the + :func:`_sql.select` function. See that function for details. + + .. seealso:: + + :func:`_sql.select` + + :ref:`coretutorial_selecting` - in the Core tutorial + """ __visit_name__ = "select" @@ -3821,7 +3861,7 @@ class Select( _from_obj = () _auto_correlate = True - compile_options = SelectState.default_select_compile_options + _compile_options = SelectState.default_select_compile_options _traverse_internals = ( [ @@ -3851,7 +3891,7 @@ class Select( ) _cache_key_traversal = _traverse_internals + [ - ("compile_options", InternalTraversal.dp_has_cache_key) + ("_compile_options", InternalTraversal.dp_has_cache_key) ] @classmethod @@ -4274,12 +4314,35 @@ class Select( @property def column_descriptions(self): """Return a 'column descriptions' structure which may be - plugin-specific. + :term:`plugin-specific`. """ meth = SelectState.get_plugin_class(self).get_column_descriptions return meth(self) + def from_statement(self, statement): + """Apply the columns which this :class:`.Select` would select + onto another statement. + + This operation is :term:`plugin-specific` and will raise a not + supported exception if this :class:`_sql.Select` does not select from + plugin-enabled entities. + + + The statement is typically either a :func:`_expression.text` or + :func:`_expression.select` construct, and should return the set of + columns appropriate to the entities represented by this + :class:`.Select`. + + .. seealso:: + + :ref:`orm_tutorial_literal_sql` - usage examples in the + ORM tutorial + + """ + meth = SelectState.get_plugin_class(self).from_statement + return meth(self, statement) + @_generative def join(self, target, onclause=None, isouter=False, full=False): r"""Create a SQL JOIN against this :class:`_expresson.Select` @@ -4550,7 +4613,7 @@ class Select( ) @_generative - def with_only_columns(self, columns): + def with_only_columns(self, *columns): r"""Return a new :func:`_expression.select` construct with its columns clause replaced with the given columns. @@ -4558,65 +4621,26 @@ class Select( :func:`_expression.select` had been called with the given columns clause. I.e. a statement:: - s = select([table1.c.a, table1.c.b]) - s = s.with_only_columns([table1.c.b]) + s = select(table1.c.a, table1.c.b) + s = s.with_only_columns(table1.c.b) should be exactly equivalent to:: - s = select([table1.c.b]) - - This means that FROM clauses which are only derived - from the column list will be discarded if the new column - list no longer contains that FROM:: - - >>> table1 = table('t1', column('a'), column('b')) - >>> table2 = table('t2', column('a'), column('b')) - >>> s1 = select([table1.c.a, table2.c.b]) - >>> print(s1) - SELECT t1.a, t2.b FROM t1, t2 - >>> s2 = s1.with_only_columns([table2.c.b]) - >>> print(s2) - SELECT t2.b FROM t1 - - The preferred way to maintain a specific FROM clause - in the construct, assuming it won't be represented anywhere - else (i.e. not in the WHERE clause, etc.) is to set it using - :meth:`_expression.Select.select_from`:: - - >>> s1 = select([table1.c.a, table2.c.b]).\ - ... select_from(table1.join(table2, - ... table1.c.a==table2.c.a)) - >>> s2 = s1.with_only_columns([table2.c.b]) - >>> print(s2) - SELECT t2.b FROM t1 JOIN t2 ON t1.a=t2.a - - Care should also be taken to use the correct set of column objects - passed to :meth:`_expression.Select.with_only_columns`. - Since the method is - essentially equivalent to calling the :func:`_expression.select` - construct in the first place with the given columns, the columns passed - to :meth:`_expression.Select.with_only_columns` - should usually be a subset of - those which were passed to the :func:`_expression.select` - construct, not those which are available from the ``.c`` collection of - that :func:`_expression.select`. That is:: - - s = select([table1.c.a, table1.c.b]).select_from(table1) - s = s.with_only_columns([table1.c.b]) - - and **not**:: - - # usually incorrect - s = s.with_only_columns([s.c.b]) - - The latter would produce the SQL:: - - SELECT b - FROM (SELECT t1.a AS a, t1.b AS b - FROM t1), t1 - - Since the :func:`_expression.select` construct is essentially - being asked to select both from ``table1`` as well as itself. + s = select(table1.c.b) + + Note that this will also dynamically alter the FROM clause of the + statement if it is not explicitly stated. To maintain the FROM + clause, ensure the :meth:`_sql.Select.select_from` method is + used appropriately:: + + s = select(table1.c.a, table2.c.b) + s = s.select_from(table2.c.b).with_only_columns(table1.c.a) + + :param \*columns: column expressions to be used. + + .. versionchanged:: 1.4 the :meth:`_sql.Select.with_only_columns` + method accepts the list of column expressions positionally; + passing the expressions as a list is deprecateed. """ @@ -4626,7 +4650,9 @@ class Select( self._assert_no_memoizations() rc = [] - for c in columns: + for c in coercions._expression_collection_was_a_list( + "columns", "Select.with_only_columns", columns + ): c = coercions.expect(roles.ColumnsClauseRole, c,) # TODO: why are we doing this here? if isinstance(c, ScalarSelect): diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index 1ce59431e..ecc6a4ab8 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -404,6 +404,9 @@ class AssertsCompiledSQL(object): from sqlalchemy import orm + if isinstance(clause, orm.dynamic.AppenderQuery): + clause = clause._statement + if isinstance(clause, orm.Query): compile_state = clause._compile_state() compile_state.statement._label_style = ( |