codekingpro/portable-devtools
115k
1# orm/session.py
2# Copyright (C) 2005-2026 the SQLAlchemy authors and contributors
3# <see AUTHORS file>
4#
5# This module is part of SQLAlchemy and is released under
6# the MIT License: https://www.opensource.org/licenses/mit-license.php
7
8"""Provides the Session class and related utilities."""
9
10from __future__ import annotations
11
12import contextlib
13from enum import Enum
14import itertools
15import sys
16import typing
17from typing import Any
18from typing import Callable
19from typing import cast
20from typing import Dict
21from typing import Generic
22from typing import Iterable
23from typing import Iterator
24from typing import List
25from typing import NoReturn
26from typing import Optional
27from typing import overload
28from typing import Sequence
29from typing import Set
30from typing import Tuple
31from typing import Type
32from typing import TYPE_CHECKING
33from typing import TypeVar
34from typing import Union
35import weakref
36
37from . import attributes
38from . import bulk_persistence
39from . import context
40from . import descriptor_props
41from . import exc
42from . import identity
43from . import loading
44from . import query
45from . import state as statelib
46from ._typing import _O
47from ._typing import insp_is_mapper
48from ._typing import is_composite_class
49from ._typing import is_orm_option
50from ._typing import is_user_defined_option
51from .base import _class_to_mapper
52from .base import _none_set
53from .base import _state_mapper
54from .base import instance_str
55from .base import LoaderCallableStatus
56from .base import object_mapper
57from .base import object_state
58from .base import PassiveFlag
59from .base import state_str
60from .context import FromStatement
61from .context import ORMCompileState
62from .identity import IdentityMap
63from .query import Query
64from .state import InstanceState
65from .state_changes import _StateChange
66from .state_changes import _StateChangeState
67from .state_changes import _StateChangeStates
68from .unitofwork import UOWTransaction
69from .. import engine
70from .. import exc as sa_exc
71from .. import sql
72from .. import util
73from ..engine import Connection
74from ..engine import Engine
75from ..engine.util import TransactionalContext
76from ..event import dispatcher
77from ..event import EventTarget
78from ..inspection import inspect
79from ..inspection import Inspectable
80from ..sql import coercions
81from ..sql import dml
82from ..sql import roles
83from ..sql import Select
84from ..sql import TableClause
85from ..sql import visitors
86from ..sql.base import _NoArg
87from ..sql.base import CompileState
88from ..sql.schema import Table
89from ..sql.selectable import ForUpdateArg
90from ..sql.selectable import LABEL_STYLE_TABLENAME_PLUS_COL
91from ..util import IdentitySet
92from ..util.typing import Literal
93from ..util.typing import Protocol
94
95if typing.TYPE_CHECKING:
96 from ._typing import _EntityType
97 from ._typing import _IdentityKeyType
98 from ._typing import _InstanceDict
99 from ._typing import OrmExecuteOptionsParameter
100 from .interfaces import ORMOption
101 from .interfaces import UserDefinedOption
102 from .mapper import Mapper
103 from .path_registry import PathRegistry
104 from .query import RowReturningQuery
105 from ..engine import Result
106 from ..engine import Row
107 from ..engine import RowMapping
108 from ..engine.base import Transaction
109 from ..engine.base import TwoPhaseTransaction
110 from ..engine.interfaces import _CoreAnyExecuteParams
111 from ..engine.interfaces import _CoreSingleExecuteParams
112 from ..engine.interfaces import _ExecuteOptions
113 from ..engine.interfaces import CoreExecuteOptionsParameter
114 from ..engine.result import ScalarResult
115 from ..event import _InstanceLevelDispatch
116 from ..sql._typing import _ColumnsClauseArgument
117 from ..sql._typing import _InfoType
118 from ..sql._typing import _T0
119 from ..sql._typing import _T1
120 from ..sql._typing import _T2
121 from ..sql._typing import _T3
122 from ..sql._typing import _T4
123 from ..sql._typing import _T5
124 from ..sql._typing import _T6
125 from ..sql._typing import _T7
126 from ..sql._typing import _TypedColumnClauseArgument as _TCCA
127 from ..sql.base import Executable
128 from ..sql.base import ExecutableOption
129 from ..sql.elements import ClauseElement
130 from ..sql.roles import TypedColumnsClauseRole
131 from ..sql.selectable import ForUpdateParameter
132 from ..sql.selectable import TypedReturnsRows
133
134_T = TypeVar("_T", bound=Any)
135
136__all__ = [
137 "Session",
138 "SessionTransaction",
139 "sessionmaker",
140 "ORMExecuteState",
141 "close_all_sessions",
142 "make_transient",
143 "make_transient_to_detached",
144 "object_session",
145]
146
147_sessions: weakref.WeakValueDictionary[int, Session] = (
148 weakref.WeakValueDictionary()
149)
150"""Weak-referencing dictionary of :class:`.Session` objects.
151"""
152
153statelib._sessions = _sessions
154
155_PKIdentityArgument = Union[Any, Tuple[Any, ...]]
156
157_BindArguments = Dict[str, Any]
158
159_EntityBindKey = Union[Type[_O], "Mapper[_O]"]
160_SessionBindKey = Union[Type[Any], "Mapper[Any]", "TableClause", str]
161_SessionBind = Union["Engine", "Connection"]
162
163JoinTransactionMode = Literal[
164 "conditional_savepoint",
165 "rollback_only",
166 "control_fully",
167 "create_savepoint",
168]
169
170
171class _ConnectionCallableProto(Protocol):
172 """a callable that returns a :class:`.Connection` given an instance.
173
174 This callable, when present on a :class:`.Session`, is called only from the
175 ORM's persistence mechanism (i.e. the unit of work flush process) to allow
176 for connection-per-instance schemes (i.e. horizontal sharding) to be used
177 as persistence time.
178
179 This callable is not present on a plain :class:`.Session`, however
180 is established when using the horizontal sharding extension.
181
182 """
183
184 def __call__(
185 self,
186 mapper: Optional[Mapper[Any]] = None,
187 instance: Optional[object] = None,
188 **kw: Any,
189 ) -> Connection: ...
190
191
192def _state_session(state: InstanceState[Any]) -> Optional[Session]:
193 """Given an :class:`.InstanceState`, return the :class:`.Session`
194 associated, if any.
195 """
196 return state.session
197
198
199class _SessionClassMethods:
200 """Class-level methods for :class:`.Session`, :class:`.sessionmaker`."""
201
202 @classmethod
203 @util.deprecated(
204 "1.3",
205 "The :meth:`.Session.close_all` method is deprecated and will be "
206 "removed in a future release. Please refer to "
207 ":func:`.session.close_all_sessions`.",
208 )
209 def close_all(cls) -> None:
210 """Close *all* sessions in memory."""
211
212 close_all_sessions()
213
214 @classmethod
215 @util.preload_module("sqlalchemy.orm.util")
216 def identity_key(
217 cls,
218 class_: Optional[Type[Any]] = None,
219 ident: Union[Any, Tuple[Any, ...]] = None,
220 *,
221 instance: Optional[Any] = None,
222 row: Optional[Union[Row[Any], RowMapping]] = None,
223 identity_token: Optional[Any] = None,
224 ) -> _IdentityKeyType[Any]:
225 """Return an identity key.
226
227 This is an alias of :func:`.util.identity_key`.
228
229 """
230 return util.preloaded.orm_util.identity_key(
231 class_,
232 ident,
233 instance=instance,
234 row=row,
235 identity_token=identity_token,
236 )
237
238 @classmethod
239 def object_session(cls, instance: object) -> Optional[Session]:
240 """Return the :class:`.Session` to which an object belongs.
241
242 This is an alias of :func:`.object_session`.
243
244 """
245
246 return object_session(instance)
247
248
249class SessionTransactionState(_StateChangeState):
250 ACTIVE = 1
251 PREPARED = 2
252 COMMITTED = 3
253 DEACTIVE = 4
254 CLOSED = 5
255 PROVISIONING_CONNECTION = 6
256
257
258# backwards compatibility
259ACTIVE, PREPARED, COMMITTED, DEACTIVE, CLOSED, PROVISIONING_CONNECTION = tuple(
260 SessionTransactionState
261)
262
263
264class ORMExecuteState(util.MemoizedSlots):
265 """Represents a call to the :meth:`_orm.Session.execute` method, as passed
266 to the :meth:`.SessionEvents.do_orm_execute` event hook.
267
268 .. versionadded:: 1.4
269
270 .. seealso::
271
272 :ref:`session_execute_events` - top level documentation on how
273 to use :meth:`_orm.SessionEvents.do_orm_execute`
274
275 """
276
277 __slots__ = (
278 "session",
279 "statement",
280 "parameters",
281 "execution_options",
282 "local_execution_options",
283 "bind_arguments",
284 "identity_token",
285 "_compile_state_cls",
286 "_starting_event_idx",
287 "_events_todo",
288 "_update_execution_options",
289 )
290
291 session: Session
292 """The :class:`_orm.Session` in use."""
293
294 statement: Executable
295 """The SQL statement being invoked.
296
297 For an ORM selection as would
298 be retrieved from :class:`_orm.Query`, this is an instance of
299 :class:`_sql.select` that was generated from the ORM query.
300 """
301
302 parameters: Optional[_CoreAnyExecuteParams]
303 """Dictionary of parameters that was passed to
304 :meth:`_orm.Session.execute`."""
305
306 execution_options: _ExecuteOptions
307 """The complete dictionary of current execution options.
308
309 This is a merge of the statement level options with the
310 locally passed execution options.
311
312 .. seealso::
313
314 :attr:`_orm.ORMExecuteState.local_execution_options`
315
316 :meth:`_sql.Executable.execution_options`
317
318 :ref:`orm_queryguide_execution_options`
319
320 """
321
322 local_execution_options: _ExecuteOptions
323 """Dictionary view of the execution options passed to the
324 :meth:`.Session.execute` method.
325
326 This does not include options that may be associated with the statement
327 being invoked.
328
329 .. seealso::
330
331 :attr:`_orm.ORMExecuteState.execution_options`
332
333 """
334
335 bind_arguments: _BindArguments
336 """The dictionary passed as the
337 :paramref:`_orm.Session.execute.bind_arguments` dictionary.
338
339 This dictionary may be used by extensions to :class:`_orm.Session` to pass
340 arguments that will assist in determining amongst a set of database
341 connections which one should be used to invoke this statement.
342
343 """
344
345 _compile_state_cls: Optional[Type[ORMCompileState]]
346 _starting_event_idx: int
347 _events_todo: List[Any]
348 _update_execution_options: Optional[_ExecuteOptions]
349
350 def __init__(
351 self,
352 session: Session,
353 statement: Executable,
354 parameters: Optional[_CoreAnyExecuteParams],
355 execution_options: _ExecuteOptions,
356 bind_arguments: _BindArguments,
357 compile_state_cls: Optional[Type[ORMCompileState]],
358 events_todo: List[_InstanceLevelDispatch[Session]],
359 ):
360 """Construct a new :class:`_orm.ORMExecuteState`.
361
362 this object is constructed internally.
363
364 """
365 self.session = session
366 self.statement = statement
367 self.parameters = parameters
368 self.local_execution_options = execution_options
369 self.execution_options = statement._execution_options.union(
370 execution_options
371 )
372 self.bind_arguments = bind_arguments
373 self._compile_state_cls = compile_state_cls
374 self._events_todo = list(events_todo)
375
376 def _remaining_events(self) -> List[_InstanceLevelDispatch[Session]]:
377 return self._events_todo[self._starting_event_idx + 1 :]
378
379 def invoke_statement(
380 self,
381 statement: Optional[Executable] = None,
382 params: Optional[_CoreAnyExecuteParams] = None,
383 execution_options: Optional[OrmExecuteOptionsParameter] = None,
384 bind_arguments: Optional[_BindArguments] = None,
385 ) -> Result[Any]:
386 """Execute the statement represented by this
387 :class:`.ORMExecuteState`, without re-invoking events that have
388 already proceeded.
389
390 This method essentially performs a re-entrant execution of the current
391 statement for which the :meth:`.SessionEvents.do_orm_execute` event is
392 being currently invoked. The use case for this is for event handlers
393 that want to override how the ultimate
394 :class:`_engine.Result` object is returned, such as for schemes that
395 retrieve results from an offline cache or which concatenate results
396 from multiple executions.
397
398 When the :class:`_engine.Result` object is returned by the actual
399 handler function within :meth:`_orm.SessionEvents.do_orm_execute` and
400 is propagated to the calling
401 :meth:`_orm.Session.execute` method, the remainder of the
402 :meth:`_orm.Session.execute` method is preempted and the
403 :class:`_engine.Result` object is returned to the caller of
404 :meth:`_orm.Session.execute` immediately.
405
406 :param statement: optional statement to be invoked, in place of the
407 statement currently represented by :attr:`.ORMExecuteState.statement`.
408
409 :param params: optional dictionary of parameters or list of parameters
410 which will be merged into the existing
411 :attr:`.ORMExecuteState.parameters` of this :class:`.ORMExecuteState`.
412
413 .. versionchanged:: 2.0 a list of parameter dictionaries is accepted
414 for executemany executions.
415
416 :param execution_options: optional dictionary of execution options
417 will be merged into the existing
418 :attr:`.ORMExecuteState.execution_options` of this
419 :class:`.ORMExecuteState`.
420
421 :param bind_arguments: optional dictionary of bind_arguments
422 which will be merged amongst the current
423 :attr:`.ORMExecuteState.bind_arguments`
424 of this :class:`.ORMExecuteState`.
425
426 :return: a :class:`_engine.Result` object with ORM-level results.
427
428 .. seealso::
429
430 :ref:`do_orm_execute_re_executing` - background and examples on the
431 appropriate usage of :meth:`_orm.ORMExecuteState.invoke_statement`.
432
433
434 """
435
436 if statement is None:
437 statement = self.statement
438
439 _bind_arguments = dict(self.bind_arguments)
440 if bind_arguments:
441 _bind_arguments.update(bind_arguments)
442 _bind_arguments["_sa_skip_events"] = True
443
444 _params: Optional[_CoreAnyExecuteParams]
445 if params:
446 if self.is_executemany:
447 _params = []
448 exec_many_parameters = cast(
449 "List[Dict[str, Any]]", self.parameters
450 )
451 for _existing_params, _new_params in itertools.zip_longest(
452 exec_many_parameters,
453 cast("List[Dict[str, Any]]", params),
454 ):
455 if _existing_params is None or _new_params is None:
456 raise sa_exc.InvalidRequestError(
457 f"Can't apply executemany parameters to "
458 f"statement; number of parameter sets passed to "
459 f"Session.execute() ({len(exec_many_parameters)}) "
460 f"does not match number of parameter sets given "
461 f"to ORMExecuteState.invoke_statement() "
462 f"({len(params)})"
463 )
464 _existing_params = dict(_existing_params)
465 _existing_params.update(_new_params)
466 _params.append(_existing_params)
467 else:
468 _params = dict(cast("Dict[str, Any]", self.parameters))
469 _params.update(cast("Dict[str, Any]", params))
470 else:
471 _params = self.parameters
472
473 _execution_options = self.local_execution_options
474 if execution_options:
475 _execution_options = _execution_options.union(execution_options)
476
477 return self.session._execute_internal(
478 statement,
479 _params,
480 execution_options=_execution_options,
481 bind_arguments=_bind_arguments,
482 _parent_execute_state=self,
483 )
484
485 @property
486 def bind_mapper(self) -> Optional[Mapper[Any]]:
487 """Return the :class:`_orm.Mapper` that is the primary "bind" mapper.
488
489 For an :class:`_orm.ORMExecuteState` object invoking an ORM
490 statement, that is, the :attr:`_orm.ORMExecuteState.is_orm_statement`
491 attribute is ``True``, this attribute will return the
492 :class:`_orm.Mapper` that is considered to be the "primary" mapper
493 of the statement. The term "bind mapper" refers to the fact that
494 a :class:`_orm.Session` object may be "bound" to multiple
495 :class:`_engine.Engine` objects keyed to mapped classes, and the
496 "bind mapper" determines which of those :class:`_engine.Engine` objects
497 would be selected.
498
499 For a statement that is invoked against a single mapped class,
500 :attr:`_orm.ORMExecuteState.bind_mapper` is intended to be a reliable
501 way of getting this mapper.
502
503 .. versionadded:: 1.4.0b2
504
505 .. seealso::
506
507 :attr:`_orm.ORMExecuteState.all_mappers`
508
509
510 """
511 mp: Optional[Mapper[Any]] = self.bind_arguments.get("mapper", None)
512 return mp
513
514 @property
515 def all_mappers(self) -> Sequence[Mapper[Any]]:
516 """Return a sequence of all :class:`_orm.Mapper` objects that are
517 involved at the top level of this statement.
518
519 By "top level" we mean those :class:`_orm.Mapper` objects that would
520 be represented in the result set rows for a :func:`_sql.select`
521 query, or for a :func:`_dml.update` or :func:`_dml.delete` query,
522 the mapper that is the main subject of the UPDATE or DELETE.
523
524 .. versionadded:: 1.4.0b2
525
526 .. seealso::
527
528 :attr:`_orm.ORMExecuteState.bind_mapper`
529
530
531
532 """
533 if not self.is_orm_statement:
534 return []
535 elif isinstance(self.statement, (Select, FromStatement)):
536 result = []
537 seen = set()
538 for d in self.statement.column_descriptions:
539 ent = d["entity"]
540 if ent:
541 insp = inspect(ent, raiseerr=False)
542 if insp and insp.mapper and insp.mapper not in seen:
543 seen.add(insp.mapper)
544 result.append(insp.mapper)
545 return result
546 elif self.statement.is_dml and self.bind_mapper:
547 return [self.bind_mapper]
548 else:
549 return []
550
551 @property
552 def is_orm_statement(self) -> bool:
553 """return True if the operation is an ORM statement.
554
555 This indicates that the select(), insert(), update(), or delete()
556 being invoked contains ORM entities as subjects. For a statement
557 that does not have ORM entities and instead refers only to
558 :class:`.Table` metadata, it is invoked as a Core SQL statement
559 and no ORM-level automation takes place.
560
561 """
562 return self._compile_state_cls is not None
563
564 @property
565 def is_executemany(self) -> bool:
566 """return True if the parameters are a multi-element list of
567 dictionaries with more than one dictionary.
568
569 .. versionadded:: 2.0
570
571 """
572 return isinstance(self.parameters, list)
573
574 @property
575 def is_select(self) -> bool:
576 """return True if this is a SELECT operation.
577
578 .. versionchanged:: 2.0.30 - the attribute is also True for a
579 :meth:`_sql.Select.from_statement` construct that is itself against
580 a :class:`_sql.Select` construct, such as
581 ``select(Entity).from_statement(select(..))``
582
583 """
584 return self.statement.is_select
585
586 @property
587 def is_from_statement(self) -> bool:
588 """return True if this operation is a
589 :meth:`_sql.Select.from_statement` operation.
590
591 This is independent from :attr:`_orm.ORMExecuteState.is_select`, as a
592 ``select().from_statement()`` construct can be used with
593 INSERT/UPDATE/DELETE RETURNING types of statements as well.
594 :attr:`_orm.ORMExecuteState.is_select` will only be set if the
595 :meth:`_sql.Select.from_statement` is itself against a
596 :class:`_sql.Select` construct.
597
598 .. versionadded:: 2.0.30
599
600 """
601 return self.statement.is_from_statement
602
603 @property
604 def is_insert(self) -> bool:
605 """return True if this is an INSERT operation.
606
607 .. versionchanged:: 2.0.30 - the attribute is also True for a
608 :meth:`_sql.Select.from_statement` construct that is itself against
609 a :class:`_sql.Insert` construct, such as
610 ``select(Entity).from_statement(insert(..))``
611
612 """
613 return self.statement.is_dml and self.statement.is_insert
614
615 @property
616 def is_update(self) -> bool:
617 """return True if this is an UPDATE operation.
618
619 .. versionchanged:: 2.0.30 - the attribute is also True for a
620 :meth:`_sql.Select.from_statement` construct that is itself against
621 a :class:`_sql.Update` construct, such as
622 ``select(Entity).from_statement(update(..))``
623
624 """
625 return self.statement.is_dml and self.statement.is_update
626
627 @property
628 def is_delete(self) -> bool:
629 """return True if this is a DELETE operation.
630
631 .. versionchanged:: 2.0.30 - the attribute is also True for a
632 :meth:`_sql.Select.from_statement` construct that is itself against
633 a :class:`_sql.Delete` construct, such as
634 ``select(Entity).from_statement(delete(..))``
635
636 """
637 return self.statement.is_dml and self.statement.is_delete
638
639 @property
640 def _is_crud(self) -> bool:
641 return isinstance(self.statement, (dml.Update, dml.Delete))
642
643 def update_execution_options(self, **opts: Any) -> None:
644 """Update the local execution options with new values."""
645 self.local_execution_options = self.local_execution_options.union(opts)
646
647 def _orm_compile_options(
648 self,
649 ) -> Optional[
650 Union[
651 context.ORMCompileState.default_compile_options,
652 Type[context.ORMCompileState.default_compile_options],
653 ]
654 ]:
655 if not self.is_select:
656 return None
657 try:
658 opts = self.statement._compile_options
659 except AttributeError:
660 return None
661
662 if opts is not None and opts.isinstance(
663 context.ORMCompileState.default_compile_options
664 ):
665 return opts # type: ignore
666 else:
667 return None
668
669 @property
670 def lazy_loaded_from(self) -> Optional[InstanceState[Any]]:
671 """An :class:`.InstanceState` that is using this statement execution
672 for a lazy load operation.
673
674 The primary rationale for this attribute is to support the horizontal
675 sharding extension, where it is available within specific query
676 execution time hooks created by this extension. To that end, the
677 attribute is only intended to be meaningful at **query execution
678 time**, and importantly not any time prior to that, including query
679 compilation time.
680
681 """
682 return self.load_options._lazy_loaded_from
683
684 @property
685 def loader_strategy_path(self) -> Optional[PathRegistry]:
686 """Return the :class:`.PathRegistry` for the current load path.
687
688 This object represents the "path" in a query along relationships
689 when a particular object or collection is being loaded.
690
691 """
692 opts = self._orm_compile_options()
693 if opts is not None:
694 return opts._current_path
695 else:
696 return None
697
698 @property
699 def is_column_load(self) -> bool:
700 """Return True if the operation is refreshing column-oriented
701 attributes on an existing ORM object.
702
703 This occurs during operations such as :meth:`_orm.Session.refresh`,
704 as well as when an attribute deferred by :func:`_orm.defer` is
705 being loaded, or an attribute that was expired either directly
706 by :meth:`_orm.Session.expire` or via a commit operation is being
707 loaded.
708
709 Handlers will very likely not want to add any options to queries
710 when such an operation is occurring as the query should be a straight
711 primary key fetch which should not have any additional WHERE criteria,
712 and loader options travelling with the instance
713 will have already been added to the query.
714
715 .. versionadded:: 1.4.0b2
716
717 .. seealso::
718
719 :attr:`_orm.ORMExecuteState.is_relationship_load`
720
721 """
722 opts = self._orm_compile_options()
723 return opts is not None and opts._for_refresh_state
724
725 @property
726 def is_relationship_load(self) -> bool:
727 """Return True if this load is loading objects on behalf of a
728 relationship.
729
730 This means, the loader in effect is either a LazyLoader,
731 SelectInLoader, SubqueryLoader, or similar, and the entire
732 SELECT statement being emitted is on behalf of a relationship
733 load.
734
735 Handlers will very likely not want to add any options to queries
736 when such an operation is occurring, as loader options are already
737 capable of being propagated to relationship loaders and should
738 be already present.
739
740 .. seealso::
741
742 :attr:`_orm.ORMExecuteState.is_column_load`
743
744 """
745 opts = self._orm_compile_options()
746 if opts is None:
747 return False
748 path = self.loader_strategy_path
749 return path is not None and not path.is_root
750
751 @property
752 def load_options(
753 self,
754 ) -> Union[
755 context.QueryContext.default_load_options,
756 Type[context.QueryContext.default_load_options],
757 ]:
758 """Return the load_options that will be used for this execution."""
759
760 if not self.is_select:
761 raise sa_exc.InvalidRequestError(
762 "This ORM execution is not against a SELECT statement "
763 "so there are no load options."
764 )
765
766 lo: Union[
767 context.QueryContext.default_load_options,
768 Type[context.QueryContext.default_load_options],
769 ] = self.execution_options.get(
770 "_sa_orm_load_options", context.QueryContext.default_load_options
771 )
772 return lo
773
774 @property
775 def update_delete_options(
776 self,
777 ) -> Union[
778 bulk_persistence.BulkUDCompileState.default_update_options,
779 Type[bulk_persistence.BulkUDCompileState.default_update_options],
780 ]:
781 """Return the update_delete_options that will be used for this
782 execution."""
783
784 if not self._is_crud:
785 raise sa_exc.InvalidRequestError(
786 "This ORM execution is not against an UPDATE or DELETE "
787 "statement so there are no update options."
788 )
789 uo: Union[
790 bulk_persistence.BulkUDCompileState.default_update_options,
791 Type[bulk_persistence.BulkUDCompileState.default_update_options],
792 ] = self.execution_options.get(
793 "_sa_orm_update_options",
794 bulk_persistence.BulkUDCompileState.default_update_options,
795 )
796 return uo
797
798 @property
799 def _non_compile_orm_options(self) -> Sequence[ORMOption]:
800 return [
801 opt
802 for opt in self.statement._with_options
803 if is_orm_option(opt) and not opt._is_compile_state
804 ]
805
806 @property
807 def user_defined_options(self) -> Sequence[UserDefinedOption]:
808 """The sequence of :class:`.UserDefinedOptions` that have been
809 associated with the statement being invoked.
810
811 """
812 return [
813 opt
814 for opt in self.statement._with_options
815 if is_user_defined_option(opt)
816 ]
817
818
819class SessionTransactionOrigin(Enum):
820 """indicates the origin of a :class:`.SessionTransaction`.
821
822 This enumeration is present on the
823 :attr:`.SessionTransaction.origin` attribute of any
824 :class:`.SessionTransaction` object.
825
826 .. versionadded:: 2.0
827
828 """
829
830 AUTOBEGIN = 0
831 """transaction were started by autobegin"""
832
833 BEGIN = 1
834 """transaction were started by calling :meth:`_orm.Session.begin`"""
835
836 BEGIN_NESTED = 2
837 """transaction were started by :meth:`_orm.Session.begin_nested`"""
838
839 SUBTRANSACTION = 3
840 """transaction is an internal "subtransaction" """
841
842
843class SessionTransaction(_StateChange, TransactionalContext):
844 """A :class:`.Session`-level transaction.
845
846 :class:`.SessionTransaction` is produced from the
847 :meth:`_orm.Session.begin`
848 and :meth:`_orm.Session.begin_nested` methods. It's largely an internal
849 object that in modern use provides a context manager for session
850 transactions.
851
852 Documentation on interacting with :class:`_orm.SessionTransaction` is
853 at: :ref:`unitofwork_transaction`.
854
855
856 .. versionchanged:: 1.4 The scoping and API methods to work with the
857 :class:`_orm.SessionTransaction` object directly have been simplified.
858
859 .. seealso::
860
861 :ref:`unitofwork_transaction`
862
863 :meth:`.Session.begin`
864
865 :meth:`.Session.begin_nested`
866
867 :meth:`.Session.rollback`
868
869 :meth:`.Session.commit`
870
871 :meth:`.Session.in_transaction`
872
873 :meth:`.Session.in_nested_transaction`
874
875 :meth:`.Session.get_transaction`
876
877 :meth:`.Session.get_nested_transaction`
878
879
880 """
881
882 _rollback_exception: Optional[BaseException] = None
883
884 _connections: Dict[
885 Union[Engine, Connection], Tuple[Connection, Transaction, bool, bool]
886 ]
887 session: Session
888 _parent: Optional[SessionTransaction]
889
890 _state: SessionTransactionState
891
892 _new: weakref.WeakKeyDictionary[InstanceState[Any], object]
893 _deleted: weakref.WeakKeyDictionary[InstanceState[Any], object]
894 _dirty: weakref.WeakKeyDictionary[InstanceState[Any], object]
895 _key_switches: weakref.WeakKeyDictionary[
896 InstanceState[Any], Tuple[Any, Any]
897 ]
898
899 origin: SessionTransactionOrigin
900 """Origin of this :class:`_orm.SessionTransaction`.
901
902 Refers to a :class:`.SessionTransactionOrigin` instance which is an
903 enumeration indicating the source event that led to constructing
904 this :class:`_orm.SessionTransaction`.
905
906 .. versionadded:: 2.0
907
908 """
909
910 nested: bool = False
911 """Indicates if this is a nested, or SAVEPOINT, transaction.
912
913 When :attr:`.SessionTransaction.nested` is True, it is expected
914 that :attr:`.SessionTransaction.parent` will be present as well,
915 linking to the enclosing :class:`.SessionTransaction`.
916
917 .. seealso::
918
919 :attr:`.SessionTransaction.origin`
920
921 """
922
923 def __init__(
924 self,
925 session: Session,
926 origin: SessionTransactionOrigin,
927 parent: Optional[SessionTransaction] = None,
928 ):
929 TransactionalContext._trans_ctx_check(session)
930
931 self.session = session
932 self._connections = {}
933 self._parent = parent
934 self.nested = nested = origin is SessionTransactionOrigin.BEGIN_NESTED
935 self.origin = origin
936
937 if session._close_state is _SessionCloseState.CLOSED:
938 raise sa_exc.InvalidRequestError(
939 "This Session has been permanently closed and is unable "
940 "to handle any more transaction requests."
941 )
942
943 if nested:
944 if not parent:
945 raise sa_exc.InvalidRequestError(
946 "Can't start a SAVEPOINT transaction when no existing "
947 "transaction is in progress"
948 )
949
950 self._previous_nested_transaction = session._nested_transaction
951 elif origin is SessionTransactionOrigin.SUBTRANSACTION:
952 assert parent is not None
953 else:
954 assert parent is None
955
956 self._state = SessionTransactionState.ACTIVE
957
958 self._take_snapshot()
959
960 # make sure transaction is assigned before we call the
961 # dispatch
962 self.session._transaction = self
963
964 self.session.dispatch.after_transaction_create(self.session, self)
965
966 def _raise_for_prerequisite_state(
967 self, operation_name: str, state: _StateChangeState
968 ) -> NoReturn:
969 if state is SessionTransactionState.DEACTIVE:
970 if self._rollback_exception:
971 raise sa_exc.PendingRollbackError(
972 "This Session's transaction has been rolled back "
973 "due to a previous exception during flush."
974 " To begin a new transaction with this Session, "
975 "first issue Session.rollback()."
976 f" Original exception was: {self._rollback_exception}",
977 code="7s2a",
978 )
979 else:
980 raise sa_exc.InvalidRequestError(
981 "This session is in 'inactive' state, due to the "
982 "SQL transaction being rolled back; no further SQL "
983 "can be emitted within this transaction."
984 )
985 elif state is SessionTransactionState.CLOSED:
986 raise sa_exc.ResourceClosedError("This transaction is closed")
987 elif state is SessionTransactionState.PROVISIONING_CONNECTION:
988 raise sa_exc.InvalidRequestError(
989 "This session is provisioning a new connection; concurrent "
990 "operations are not permitted",
991 code="isce",
992 )
993 else:
994 raise sa_exc.InvalidRequestError(
995 f"This session is in '{state.name.lower()}' state; no "
996 "further SQL can be emitted within this transaction."
997 )
998
999 @property
1000 def parent(self) -> Optional[SessionTransaction]:
1001 """The parent :class:`.SessionTransaction` of this
1002 :class:`.SessionTransaction`.
1003
1004 If this attribute is ``None``, indicates this
1005 :class:`.SessionTransaction` is at the top of the stack, and
1006 corresponds to a real "COMMIT"/"ROLLBACK"
1007 block. If non-``None``, then this is either a "subtransaction"
1008 (an internal marker object used by the flush process) or a
1009 "nested" / SAVEPOINT transaction. If the
1010 :attr:`.SessionTransaction.nested` attribute is ``True``, then
1011 this is a SAVEPOINT, and if ``False``, indicates this a subtransaction.
1012
1013 """
1014 return self._parent
1015
1016 @property
1017 def is_active(self) -> bool:
1018 return (
1019 self.session is not None
1020 and self._state is SessionTransactionState.ACTIVE
1021 )
1022
1023 @property
1024 def _is_transaction_boundary(self) -> bool:
1025 return self.nested or not self._parent
1026
1027 @_StateChange.declare_states(
1028 (SessionTransactionState.ACTIVE,), _StateChangeStates.NO_CHANGE
1029 )
1030 def connection(
1031 self,
1032 bindkey: Optional[Mapper[Any]],
1033 execution_options: Optional[_ExecuteOptions] = None,
1034 **kwargs: Any,
1035 ) -> Connection:
1036 bind = self.session.get_bind(bindkey, **kwargs)
1037 return self._connection_for_bind(bind, execution_options)
1038
1039 @_StateChange.declare_states(
1040 (SessionTransactionState.ACTIVE,), _StateChangeStates.NO_CHANGE
1041 )
1042 def _begin(self, nested: bool = False) -> SessionTransaction:
1043 return SessionTransaction(
1044 self.session,
1045 (
1046 SessionTransactionOrigin.BEGIN_NESTED
1047 if nested
1048 else SessionTransactionOrigin.SUBTRANSACTION
1049 ),
1050 self,
1051 )
1052
1053 def _iterate_self_and_parents(
1054 self, upto: Optional[SessionTransaction] = None
1055 ) -> Iterable[SessionTransaction]:
1056 current = self
1057 result: Tuple[SessionTransaction, ...] = ()
1058 while current:
1059 result += (current,)
1060 if current._parent is upto:
1061 break
1062 elif current._parent is None:
1063 raise sa_exc.InvalidRequestError(
1064 "Transaction %s is not on the active transaction list"
1065 % (upto)
1066 )
1067 else:
1068 current = current._parent
1069
1070 return result
1071
1072 def _take_snapshot(self) -> None:
1073 if not self._is_transaction_boundary:
1074 parent = self._parent
1075 assert parent is not None
1076 self._new = parent._new
1077 self._deleted = parent._deleted
1078 self._dirty = parent._dirty
1079 self._key_switches = parent._key_switches
1080 return
1081
1082 is_begin = self.origin in (
1083 SessionTransactionOrigin.BEGIN,
1084 SessionTransactionOrigin.AUTOBEGIN,
1085 )
1086 if not is_begin and not self.session._flushing:
1087 self.session.flush()
1088
1089 self._new = weakref.WeakKeyDictionary()
1090 self._deleted = weakref.WeakKeyDictionary()
1091 self._dirty = weakref.WeakKeyDictionary()
1092 self._key_switches = weakref.WeakKeyDictionary()
1093
1094 def _restore_snapshot(self, dirty_only: bool = False) -> None:
1095 """Restore the restoration state taken before a transaction began.
1096
1097 Corresponds to a rollback.
1098
1099 """
1100 assert self._is_transaction_boundary
1101
1102 to_expunge = set(self._new).union(self.session._new)
1103 self.session._expunge_states(to_expunge, to_transient=True)
1104
1105 for s, (oldkey, newkey) in self._key_switches.items():
1106 # we probably can do this conditionally based on
1107 # if we expunged or not, but safe_discard does that anyway
1108 self.session.identity_map.safe_discard(s)
1109
1110 # restore the old key
1111 s.key = oldkey
1112
1113 # now restore the object, but only if we didn't expunge
1114 if s not in to_expunge:
1115 self.session.identity_map.replace(s)
1116
1117 for s in set(self._deleted).union(self.session._deleted):
1118 self.session._update_impl(s, revert_deletion=True)
1119
1120 assert not self.session._deleted
1121
1122 for s in self.session.identity_map.all_states():
1123 if not dirty_only or s.modified or s in self._dirty:
1124 s._expire(s.dict, self.session.identity_map._modified)
1125
1126 def _remove_snapshot(self) -> None:
1127 """Remove the restoration state taken before a transaction began.
1128
1129 Corresponds to a commit.
1130
1131 """
1132 assert self._is_transaction_boundary
1133
1134 if not self.nested and self.session.expire_on_commit:
1135 for s in self.session.identity_map.all_states():
1136 s._expire(s.dict, self.session.identity_map._modified)
1137
1138 statelib.InstanceState._detach_states(
1139 list(self._deleted), self.session
1140 )
1141 self._deleted.clear()
1142 elif self.nested:
1143 parent = self._parent
1144 assert parent is not None
1145 parent._new.update(self._new)
1146 parent._dirty.update(self._dirty)
1147 parent._deleted.update(self._deleted)
1148 parent._key_switches.update(self._key_switches)
1149
1150 @_StateChange.declare_states(
1151 (SessionTransactionState.ACTIVE,), _StateChangeStates.NO_CHANGE
1152 )
1153 def _connection_for_bind(
1154 self,
1155 bind: _SessionBind,
1156 execution_options: Optional[CoreExecuteOptionsParameter],
1157 ) -> Connection:
1158 if bind in self._connections:
1159 if execution_options:
1160 util.warn(
1161 "Connection is already established for the "
1162 "given bind; execution_options ignored"
1163 )
1164 return self._connections[bind][0]
1165
1166 self._state = SessionTransactionState.PROVISIONING_CONNECTION
1167
1168 local_connect = False
1169 should_commit = True
1170
1171 try:
1172 if self._parent:
1173 conn = self._parent._connection_for_bind(
1174 bind, execution_options
1175 )
1176 if not self.nested:
1177 return conn
1178 else:
1179 if isinstance(bind, engine.Connection):
1180 conn = bind
1181 if conn.engine in self._connections:
1182 raise sa_exc.InvalidRequestError(
1183 "Session already has a Connection associated "
1184 "for the given Connection's Engine"
1185 )
1186 else:
1187 conn = bind.connect()
1188 local_connect = True
1189
1190 try:
1191 if execution_options:
1192 conn = conn.execution_options(**execution_options)
1193
1194 transaction: Transaction
1195 if self.session.twophase and self._parent is None:
1196 # TODO: shouldn't we only be here if not
1197 # conn.in_transaction() ?
1198 # if twophase is set and conn.in_transaction(), validate
1199 # that it is in fact twophase.
1200 transaction = conn.begin_twophase()
